diff --git a/.gitignore b/.gitignore index 04e39e37c9..543a67d7ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,133 +1,141 @@ -*.obj -*.pdb -*.user -*.aps -*.pch -*.vspscc -.DS_Store -._.DS_Store -[Bb]in -[Db]ebug*/ -obj/ -[Rr]elease*/ -_ReSharper*/ -_NCrunch_*/ -*.ncrunchsolution -*.ncrunchsolution.user -*.ncrunchproject -*.crunchsolution.cache -[Tt]est[Rr]esult* -[Bb]uild[Ll]og.* -*.[Pp]ublish.xml -*.suo -[sS]ource -[sS]andbox -umbraco.config -*.vs10x -App_Data/TEMP/* -[Uu]mbraco/[Pp]resentation/[Uu]mbraco/[Pp]lugins/* -[Uu]mbraco/[Pp]resentation/[Uu]ser[Cc]ontrols/* -[Uu]mbraco/[Pp]resentation/[Ss]cripts/* -[Uu]mbraco/[Pp]resentation/[Ff]onts/* -[Uu]mbraco/[Pp]resentation/[Cc]ss/* - -src/Umbraco.Web.UI/[Cc]ss/* -src/Umbraco.Web.UI/App_Code/* -src/Umbraco.Web.UI/App_Data/* -src/Umbraco.Tests/App_Data/* -src/Umbraco.Web.UI/[Mm]edia/* -src/Umbraco.Web.UI/[Mm]aster[Pp]ages/* -src/Umbraco.Web.UI/[Mm]acro[Ss]cripts/* -!src/Umbraco.Web.UI/[Mm]acro[Ss]cripts/[Ww]eb.[Cc]onfig -src/Umbraco.Web.UI/[Xx]slt/* -src/Umbraco.Web.UI/[Ii]mages/* -src/Umbraco.Web.UI/[Ss]cripts/* -src/Umbraco.Web.UI/Web.*.config.transformed - -umbraco/presentation/umbraco/plugins/uComponents/uComponentsInstaller.ascx -umbraco/presentation/packages/uComponents/MultiNodePicker/CustomTreeService.asmx -_BuildOutput/* -*.ncrunchsolution -build/UmbracoCms.AllBinaries*zip -build/UmbracoCms.WebPI*zip -build/UmbracoCms*zip -build/UmbracoExamine.PDF*zip -build/*.nupkg -src/Umbraco.Tests/config/applications.config -src/Umbraco.Tests/config/trees.config -src/Umbraco.Web.UI/web.config -*.orig -src/Umbraco.Tests/config/404handlers.config -src/Umbraco.Web.UI/[Vv]iews/*.cshtml -src/Umbraco.Web.UI/[Vv]iews/*.vbhtml -src/Umbraco.Tests/[Cc]onfig/umbracoSettings.config -src/Umbraco.Web.UI/[Vv]iews/* -src/packages/ -src/packages/repositories.config - -src/Umbraco.Web.UI/[Ww]eb.config -*.transformed -webpihash.txt - -node_modules - -src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/umbraco.* -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/routes.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/app.dev.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/loader.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/loader.dev.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/main.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/app.js - -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.panel.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.palettes.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.loader.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.front.js -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.config.js - -src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/ -src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.js -src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.css -src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.html -src/Umbraco.Web.UI/[Uu]mbraco/[Aa]ssets/* -src/Umbraco.Web.UI.Client/[Bb]uild/* -src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/ -src/Umbraco.Web.UI/[Uu]ser[Cc]ontrols/ -build/_BuildOutput/ -src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css -tools/NDepend/ - -src/Umbraco.Web.UI/App_Plugins/* -src/*.psess -src/*.vspx -src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/routes.js.* -NDependOut/* -*.ndproj -QueryResult.htm -*.ndproj -src/Umbraco.Web.UI/[Uu]mbraco/[Aa]ssets/* -src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* -src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.html -src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.js - -src/Umbraco.Web.UI/[Cc]onfig/appSettings.config -src/Umbraco.Web.UI/[Cc]onfig/connectionStrings.config -src/Umbraco.Web.UI/umbraco/plugins/* -src/Umbraco.Web.UI/umbraco/js/init.js -build/ApiDocs/* -build/ApiDocs/Output/* -src/Umbraco.Web.UI.Client/bower_components/* -/src/Umbraco.Web.UI/Umbraco/preview - -#Ignore Rule for output of generated documentation files from Grunt docserve -src/Umbraco.Web.UI.Client/docs/api -src/*.boltdata/ -/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.loader.js -/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.palettes.js -/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.panel.js -/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.config.js -/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.front.js -src/umbraco.sln.ide/* -build/UmbracoCms.*/ -src/.vs/ +*.obj +*.pdb +*.user +*.aps +*.pch +*.vspscc +.DS_Store +._.DS_Store +[Bb]in +[Db]ebug*/ +obj/ +[Rr]elease*/ +_ReSharper*/ +_NCrunch_*/ +*.ncrunchsolution +*.ncrunchsolution.user +*.ncrunchproject +*.crunchsolution.cache +[Tt]est[Rr]esult* +[Bb]uild[Ll]og.* +*.[Pp]ublish.xml +*.suo +[sS]ource +[sS]andbox +umbraco.config +*.vs10x +App_Data/TEMP/* +[Uu]mbraco/[Pp]resentation/[Uu]mbraco/[Pp]lugins/* +[Uu]mbraco/[Pp]resentation/[Uu]ser[Cc]ontrols/* +[Uu]mbraco/[Pp]resentation/[Ss]cripts/* +[Uu]mbraco/[Pp]resentation/[Ff]onts/* +[Uu]mbraco/[Pp]resentation/[Cc]ss/* + +src/Umbraco.Web.UI/[Cc]ss/* +src/Umbraco.Web.UI/App_Code/* +src/Umbraco.Web.UI/App_Data/* +src/Umbraco.Tests/App_Data/* +src/Umbraco.Web.UI/[Mm]edia/* +src/Umbraco.Web.UI/[Mm]aster[Pp]ages/* +src/Umbraco.Web.UI/[Mm]acro[Ss]cripts/* +!src/Umbraco.Web.UI/[Mm]acro[Ss]cripts/[Ww]eb.[Cc]onfig +src/Umbraco.Web.UI/[Xx]slt/* +src/Umbraco.Web.UI/[Ii]mages/* +src/Umbraco.Web.UI/[Ss]cripts/* +src/Umbraco.Web.UI/Web.*.config.transformed + +umbraco/presentation/umbraco/plugins/uComponents/uComponentsInstaller.ascx +umbraco/presentation/packages/uComponents/MultiNodePicker/CustomTreeService.asmx +_BuildOutput/* +*.ncrunchsolution +build/UmbracoCms.AllBinaries*zip +build/UmbracoCms.WebPI*zip +build/UmbracoCms*zip +build/UmbracoExamine.PDF*zip +build/*.nupkg +src/Umbraco.Tests/config/applications.config +src/Umbraco.Tests/config/trees.config +src/Umbraco.Web.UI/web.config +*.orig +src/Umbraco.Tests/config/404handlers.config +src/Umbraco.Web.UI/[Vv]iews/*.cshtml +src/Umbraco.Web.UI/[Vv]iews/*.vbhtml +src/Umbraco.Tests/[Cc]onfig/umbracoSettings.config +src/Umbraco.Web.UI/[Vv]iews/* +src/packages/ +src/packages/repositories.config + +src/Umbraco.Web.UI/[Ww]eb.config +*.transformed +webpihash.txt + +node_modules + +src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/umbraco.* +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/routes.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/app.dev.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/loader.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/loader.dev.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/main.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/app.js + +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.panel.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.palettes.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.loader.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.front.js +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/tuning.config.js + +src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/ +src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.js +src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.css +src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.html +src/Umbraco.Web.UI/[Uu]mbraco/[Aa]ssets/* +src/Umbraco.Web.UI.Client/[Bb]uild/* +src/Umbraco.Web.UI.Client/[Bb]uild/[Bb]elle/ +src/Umbraco.Web.UI/[Uu]ser[Cc]ontrols/ +build/_BuildOutput/ +src/Umbraco.Web.UI.Client/src/[Ll]ess/*.css +tools/NDepend/ + +src/Umbraco.Web.UI/App_Plugins/* +src/*.psess +src/*.vspx +src/Umbraco.Web.UI/[Uu]mbraco/[Jj]s/routes.js.* +NDependOut/* +*.ndproj +QueryResult.htm +*.ndproj +src/Umbraco.Web.UI/[Uu]mbraco/[Aa]ssets/* +src/Umbraco.Web.UI/[Uu]mbraco/[Ll]ib/* +src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.html +src/Umbraco.Web.UI/[Uu]mbraco/[Vv]iews/**/*.js + +src/Umbraco.Web.UI/[Cc]onfig/appSettings.config +src/Umbraco.Web.UI/[Cc]onfig/connectionStrings.config +src/Umbraco.Web.UI/umbraco/plugins/* +src/Umbraco.Web.UI/umbraco/js/init.js +build/ApiDocs/* +build/ApiDocs/Output/* +src/Umbraco.Web.UI.Client/bower_components/* +/src/Umbraco.Web.UI/Umbraco/preview + +#Ignore Rule for output of generated documentation files from Grunt docserve +src/Umbraco.Web.UI.Client/docs/api +src/*.boltdata/ +/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.loader.js +/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.palettes.js +/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.panel.js +/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.config.js +/src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.front.js +src/umbraco.sln.ide/* +build/UmbracoCms.*/ +src/.vs/ +src/Umbraco.Web.UI/umbraco/js/install.loader.js +src/Umbraco.Tests/media +tools/docfx/* +apidocs/_site/* +apidocs/api/* +build/docs.zip +build/ui-docs.zip +build/csharp-docs.zip diff --git a/README.md b/README.md index d4a024ddbc..53070b6917 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,19 @@ The easiest way to get started is to run `build/build.bat` which will build both If you're interested in making changes to Belle make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md). Note that you can always [download a nightly build](http://nightly.umbraco.org/umbraco%207.0.0/) so you don't have to build the code yourself. -## Watch a five minute introduction video ## +## Watch a introduction video ## -[![ScreenShot](http://umbraco.com/images/whatisumbraco.png)](http://umbraco.org/help-and-support/video-tutorials/getting-started/what-is-umbraco) +[![ScreenShot](http://umbraco.com/images/whatisumbraco.png)](https://umbraco.tv/videos/umbraco-v7/content-editor/basics/introduction/cms-explanation/) ## Umbraco - the simple, flexible and friendly ASP.NET CMS ## -**More than 177,000 sites trust Umbraco** +**More than 350,000 sites trust Umbraco** -For the first time on the Microsoft platform 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. It's a developers dream and your users will love it too. +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. It's a developer's dream and your users will love it too. -Used by more than 177,000 active websites including [http://daviscup.com](http://daviscup.com), [http://heinz.com](http://heinz.com), [http://peugeot.com](http://peugeot.com), [http://www.hersheys.com/](http://www.hersheys.com/) and **The Official ASP.NET and IIS.NET website from Microsoft** ([http://asp.net](http://asp.net) / [http://iis.net](http://iis.net)) you can be sure that the technology is proven, stable and scales. +Used by more than 350,000 active websites including [http://daviscup.com](http://daviscup.com), [http://heinz.com](http://heinz.com), [http://peugeot.com](http://peugeot.com), [http://www.hersheys.com/](http://www.hersheys.com/) and **The Official ASP.NET and IIS.NET website from Microsoft** ([http://asp.net](http://asp.net) / [http://iis.net](http://iis.net)), you can be sure that the technology is proven, stable and scales. -To view more examples please visit [http://umbraco.com/why-umbraco/#caseStudies](http://umbraco.com/why-umbraco/#caseStudies) +To view more examples, please visit [http://umbraco.com/why-umbraco/#caseStudies](http://umbraco.com/why-umbraco/#caseStudies) ## Downloading ## @@ -35,6 +35,6 @@ If you want to contribute back to Umbraco you should check out our [guide to con ## 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](http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). +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](http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). -To view existing issues please visit [http://issues.umbraco.org](http://issues.umbraco.org) +To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org). diff --git a/apidocs/docfx.filter.yml b/apidocs/docfx.filter.yml new file mode 100644 index 0000000000..e96fbaafff --- /dev/null +++ b/apidocs/docfx.filter.yml @@ -0,0 +1,18 @@ +apiRules: + - include: + uidRegex: ^Umbraco\.Core + - exclude: + uidRegex: ^umbraco\.Web\.org + - include: + uidRegex: ^Umbraco\.Web + - exclude: + hasAttribute: + uid: System.ComponentModel.EditorBrowsableAttribute + ctorArguments: + - System.ComponentModel.EditorBrowsableState.Never + - exclude: + uidRegex: ^umbraco\. + - exclude: + uidRegex: ^CookComputing\. + - exclude: + uidRegex: ^.*$ \ No newline at end of file diff --git a/apidocs/docfx.json b/apidocs/docfx.json new file mode 100644 index 0000000000..520622a0e0 --- /dev/null +++ b/apidocs/docfx.json @@ -0,0 +1,75 @@ +{ + "metadata": [ + { + "src": [ + { + "files": [ + "Umbraco.Core/Umbraco.Core.csproj", + "Umbraco.Web/Umbraco.Web.csproj" + ], + "exclude": [ + "**/obj/**", + "**/bin/**", + "_site/**" + ], + "cwd": "../src" + } + ], + "dest": "../apidocs/api", + "filter": "../apidocs/docfx.filter.yml" + } + ], + "build": { + "content": [ + { + "files": [ + "api/**.yml", + "api/index.md" + ] + }, + { + "files": [ + "articles/**.md", + "articles/**/toc.yml", + "toc.yml", + "*.md" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "overwrite": [ + { + "files": [ + "**.md" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "globalMetadata": { + "_appTitle": "Umbraco c# Api docs", + "_enableSearch": true, + "_disableContribution": false + }, + "dest": "_site", + "template": [ + "default", "umbracotemplate" + ] + } +} \ No newline at end of file diff --git a/apidocs/index.md b/apidocs/index.md new file mode 100644 index 0000000000..b90b816312 --- /dev/null +++ b/apidocs/index.md @@ -0,0 +1,7 @@ + +# Umbraco c# API reference + +## Quick Links: + +### [Umbraco.Core](api/Umbraco.Core.html) docs +### [Umbraco.Web](api/Umbraco.Web.html) docs diff --git a/apidocs/toc.yml b/apidocs/toc.yml new file mode 100644 index 0000000000..6817825066 --- /dev/null +++ b/apidocs/toc.yml @@ -0,0 +1,5 @@ + +- name: Umbraco.Core Documentation + href: https://our.umbraco.org/apidocs/csharp/api/Umbraco.Core.html +- name: Umbraco.Web Documentation + href: https://our.umbraco.org/apidocs/csharp/api/Umbraco.Web.html \ No newline at end of file diff --git a/apidocs/umbracotemplate/partials/class.tmpl.partial b/apidocs/umbracotemplate/partials/class.tmpl.partial new file mode 100644 index 0000000000..9153a863a4 --- /dev/null +++ b/apidocs/umbracotemplate/partials/class.tmpl.partial @@ -0,0 +1,257 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + +{{^_disableContribution}} +{{#sourceurl}}{{__global.viewSource}}{{/sourceurl}} +{{/_disableContribution}} +

{{>partials/title}}

+
{{{summary}}}
+
{{{conceptual}}}
+{{#inheritance.0}} +
+
{{__global.inheritance}}
+{{#inheritance}} +
{{{specName.0.value}}}
+{{/inheritance}} +
{{item.name.0.value}}
+
+{{/inheritance.0}} +
{{__global.namespace}}:{{namespace}}
+
{{__global.assembly}}:{{assemblies.0}}.dll
+
{{__global.syntax}}
+
+
{{syntax.content.0.value}}
+
+{{#syntax.parameters.0}} +
{{__global.parameters}}
+ + + + + + + + + +{{/syntax.parameters.0}} +{{#syntax.parameters}} + + + + + +{{/syntax.parameters}} +{{#syntax.parameters.0}} + +
{{__global.type}}{{__global.name}}{{__global.description}}
{{{type.specName.0.value}}}{{{id}}}{{{description}}}
+{{/syntax.parameters.0}} +{{#syntax.return}} +
{{__global.returns}}
+ + + + + + + + + + + + + +
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
+{{/syntax.return}} +{{#syntax.typeParameters.0}} +
{{__global.typeParameters}}
+ + + + + + + + +{{/syntax.typeParameters.0}} +{{#syntax.typeParameters}} + + + + +{{/syntax.typeParameters}} +{{#syntax.typeParameters.0}} + +
{{__global.name}}{{__global.description}}
{{{id}}}{{{description}}}
+{{/syntax.typeParameters.0}} +{{#remarks}} +
{{__global.remarks}}
+
{{{remarks}}}
+{{/remarks}} +{{#example.0}} +
{{__global.examples}}
+{{/example.0}} +{{#example}} +{{{.}}} +{{/example}} +{{#children}} +

{{>partials/classSubtitle}}

+{{#children}} +{{^_disableContribution}} +{{#sourceurl}} + + {{__global.viewSource}} +{{/sourceurl}} +{{/_disableContribution}} +

{{name.0.value}}

+
{{{summary}}}
+
{{{conceptual}}}
+
{{__global.declaration}}
+{{#syntax}} +
+
{{syntax.content.0.value}}
+
+{{#parameters.0}} +
{{__global.parameters}}
+ + + + + + + + + +{{/parameters.0}} +{{#parameters}} + + + + + +{{/parameters}} +{{#parameters.0}} + +
{{__global.type}}{{__global.name}}{{__global.description}}
{{{type.specName.0.value}}}{{{id}}}{{{description}}}
+{{/parameters.0}} +{{#return}} +
{{__global.returns}}
+ + + + + + + + + + + + + +
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
+{{/return}} +{{#typeParameters.0}} +
{{__global.typeParameters}}
+ + + + + + + + +{{/typeParameters.0}} +{{#typeParameters}} + + + + +{{/typeParameters}} +{{#typeParameters.0}} + +
{{__global.name}}{{__global.description}}
{{{id}}}{{{description}}}
+{{/typeParameters.0}} +{{#fieldValue}} +
{{__global.fieldValue}}
+ + + + + + + + + + + + + +
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
+{{/fieldValue}} +{{#propertyValue}} +
{{__global.propertyValue}}
+ + + + + + + + + + + + + +
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
+{{/propertyValue}} +{{#eventType}} +
{{__global.eventType}}
+ + + + + + + + + + + + + +
{{__global.type}}{{__global.description}}
{{{type.specName.0.value}}}{{{description}}}
+{{/eventType}} +{{/syntax}} +{{#remarks}} +
{{__global.remarks}}
+
{{{remarks}}}
+{{/remarks}} +{{#example.0}} +
{{__global.examples}}
+{{/example.0}} +{{#example}} +{{{.}}} +{{/example}} +{{#exceptions.0}} +
{{__global.exceptions}}
+ + + + + + + + +{{/exceptions.0}} +{{#exceptions}} + + + + +{{/exceptions}} +{{#exceptions.0}} + +
{{__global.type}}{{__global.condition}}
{{{type.specName.0.value}}}{{{description}}}
+{{/exceptions.0}} +{{/children}} +{{/children}} diff --git a/apidocs/umbracotemplate/partials/footer.tmpl.partial b/apidocs/umbracotemplate/partials/footer.tmpl.partial new file mode 100644 index 0000000000..69f51a101f --- /dev/null +++ b/apidocs/umbracotemplate/partials/footer.tmpl.partial @@ -0,0 +1,13 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + diff --git a/apidocs/umbracotemplate/partials/head.tmpl.partial b/apidocs/umbracotemplate/partials/head.tmpl.partial new file mode 100644 index 0000000000..591e1c1885 --- /dev/null +++ b/apidocs/umbracotemplate/partials/head.tmpl.partial @@ -0,0 +1,18 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + + + + {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} + + + + {{#_description}}{{/_description}} + + + + + + + {{#_enableSearch}}{{/_enableSearch}} + diff --git a/apidocs/umbracotemplate/partials/namespace.tmpl.partial b/apidocs/umbracotemplate/partials/namespace.tmpl.partial new file mode 100644 index 0000000000..80ca30799a --- /dev/null +++ b/apidocs/umbracotemplate/partials/namespace.tmpl.partial @@ -0,0 +1,18 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + +{{^_disableContribution}} +{{#sourceurl}} +{{__global.viewSource}} +{{/sourceurl}} +{{/_disableContribution}} +

{{>partials/title}}

+
{{{summary}}}
+
{{{conceptual}}}
+
{{{remarks}}}
+{{#children}} +

{{>partials/namespaceSubtitle}}

+ {{#children}} +

{{{specName.0.value}}}

+
{{{summary}}}
+ {{/children}} +{{/children}} diff --git a/apidocs/umbracotemplate/partials/navbar.tmpl.partial b/apidocs/umbracotemplate/partials/navbar.tmpl.partial new file mode 100644 index 0000000000..e9ee0af1c7 --- /dev/null +++ b/apidocs/umbracotemplate/partials/navbar.tmpl.partial @@ -0,0 +1,22 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + + diff --git a/apidocs/umbracotemplate/partials/rest.tmpl.partial b/apidocs/umbracotemplate/partials/rest.tmpl.partial new file mode 100644 index 0000000000..4306bf7db1 --- /dev/null +++ b/apidocs/umbracotemplate/partials/rest.tmpl.partial @@ -0,0 +1,101 @@ +{{!Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} + +{{^_disableContribution}} +{{#sourceurl}}View Source{{/sourceurl}} +{{/_disableContribution}} +

{{name}}

+{{#summary}} +
{{{summary}}}
+{{/summary}} +{{#description}} +
{{{description}}}
+{{/description}} +{{#conceptual}} +
{{{conceptual}}}
+{{/conceptual}} +{{#children}} +{{^_disableContribution}} +{{#sourceurl}} + + View Source +{{/sourceurl}} +{{/_disableContribution}} +

{{operationId}}

+{{#summary}} +
{{{summary}}}
+{{/summary}} +{{#description}} +
{{{description}}}
+{{/description}} +{{#conceptual}} +
{{{conceptual}}}
+{{/conceptual}} +
Request
+
+
{{operation}} {{path}}
+
+{{#parameters.0}} +
Parameters
+ + + + + + + + + + +{{/parameters.0}} +{{#parameters}} + + + + + + + {{/parameters}} + {{#parameters.0}} + +
NameTypeValueNotes
{{#required}}*{{/required}}{{name}}{{type}}{{default}}{{{description}}}
+{{/parameters.0}} +{{#responses.0}} +
+
Responses
+ + + + + + + + + +{{/responses.0}} +{{#responses}} + + + + + + {{/responses}} + {{#responses.0}} + +
Status CodeDescriptionSamples
{{statusCode}}{{{description}}} + {{#examples}} +
+ Mime type: {{mimeType}} +
+
{{content}}
+ {{/examples}} +
+
+{{/responses.0}} +{{#footer}} + +{{/footer}} +{{/children}} +{{#footer}} + +{{/footer}} + diff --git a/apidocs/umbracotemplate/styles/main.css b/apidocs/umbracotemplate/styles/main.css new file mode 100644 index 0000000000..7756b2f7d4 --- /dev/null +++ b/apidocs/umbracotemplate/styles/main.css @@ -0,0 +1,73 @@ +body { + color: rgba(0,0,0,.8); +} +.navbar-inverse { + background: #a3db78; +} +.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text { + color: rgba(0,0,0,.8); +} + +.navbar-inverse { + border-color: transparent; +} + +.sidetoc { + background-color: #f5fbf1; +} +body .toc { + background-color: #f5fbf1; +} +.sidefilter { + background-color: #daf0c9; +} +.subnav { + background-color: #f5fbf1; +} + +.navbar-inverse .navbar-nav>.active>a { + color: rgba(0,0,0,.8); + background-color: #daf0c9; +} + +.navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { + color: rgba(0,0,0,.8); + background-color: #daf0c9; +} + +.btn-primary { + color: rgba(0,0,0,.8); + background-color: #fff; + border-color: rgba(0,0,0,.8); +} +.btn-primary:hover { + background-color: #daf0c9; + color: rgba(0,0,0,.8); + border-color: rgba(0,0,0,.8); +} + +.toc .nav > li > a { + color: rgba(0,0,0,.8); +} + +button, a { + color: #f36f21; +} + +button:hover, +button:focus, +a:hover, +a:focus { + color: #143653; + text-decoration: none; +} + +.navbar-header .navbar-brand { + background: url(https://our.umbraco.org/assets/images/logo.svg) left center no-repeat; + background-size: 40px auto; + width:50px; +} + +.toc .nav > li.active > a { + color: #f36f21; +} diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..db69da4978 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,51 @@ +version: '{build}' +shallow_clone: true +build_script: +- cmd: >- + cd build + + SET "release=" + + FOR /F "skip=1 delims=" %%i IN (UmbracoVersion.txt) DO IF NOT DEFINED release SET "release=%%i" + + SET PATH=C:\Program Files (x86)\MSBuild\14.0\Bin;%PATH% + + ECHO %PATH% + + + ECHO Building Release %release% build%APPVEYOR_BUILD_NUMBER% + + + SET nuGetFolder=%CD%\..\src\packages\ + + ..\src\.nuget\NuGet.exe sources Add -Name MyGetUmbracoCore -Source https://www.myget.org/F/umbracocore/api/v2/ >NUL + + ..\src\.nuget\NuGet.exe install ..\src\Umbraco.Web.UI\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet + + + IF EXIST ..\src\umbraco.businesslogic\packages.config ..\src\.nuget\NuGet.exe install ..\src\umbraco.businesslogic\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet + + ..\src\.nuget\NuGet.exe install ..\src\Umbraco.Core\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet + + + SET MSBUILD="C:\Program Files (x86)\MSBuild\14.0\Bin\MsBuild.exe" + + %MSBUILD% "../src/Umbraco.Tests/Umbraco.Tests.csproj" /verbosity:minimal + + + build.bat %release% build%APPVEYOR_BUILD_NUMBER% + + + ECHO %PATH% +test: + assemblies: src\Umbraco.Tests\bin\Debug\Umbraco.Tests.dll +artifacts: +- path: build\UmbracoCms.* +notifications: +- provider: Slack + auth_token: + secure: v2csJi2V5ghR0rPdODK8GJdOGNCA+XaK84iQ9MdPOClqB+VU+40ybdKp6gPirGSH + channel: '#build-umbraco-core' + on_build_success: false + on_build_failure: true + on_build_status_changed: false \ No newline at end of file diff --git a/build/ApiDocs/TOC.css b/build/ApiDocs/TOC.css deleted file mode 100644 index 9c32aba214..0000000000 --- a/build/ApiDocs/TOC.css +++ /dev/null @@ -1,170 +0,0 @@ -/* File : TOC.css -// Author : Eric Woodruff (Eric@EWoodruff.us) -// Updated : 09/07/2007 -// -// Stylesheet for the table of content -*/ - -* -{ - margin: 0px 0px 0px 0px; - padding: 0px 0px 0px 0px; -} - -body -{ - font-family: Segoe UI, Arial, Verdana, Helvetica, sans-serif; - font-size: 0.9em; - background-color: white; - color: White; - overflow: hidden; -} - -input -{ - padding:5px; - margin: 3px 0px 3px 0px -} - -img -{ - border: 0; - margin-left: 5px; - margin-right: 2px; -} - -img.TreeNodeImg -{ - cursor: pointer; -} - -img.TOCLink -{ - cursor: pointer; - margin-left: 0; - margin-right: 0; -} - -a.SelectedNode, a.UnselectedNode -{ - color: #333; - text-decoration: none; - padding: 1px 3px 1px 3px; - white-space: nowrap; -} - -a.SelectedNode -{ - background-color: #ffffff; - border: solid 1px #999999; - padding: 0px 2px 0px 2px; -} - -a.UnselectedNode:hover, a.SelectedNode:hover -{ - background-color: #cccccc; - border: solid 1px #999999; - padding: 0px 2px 0px 2px; -} - -.Visible -{ - display: block; - margin-left: 2em; -} - -.Hidden -{ - display: none; -} - -.Tree -{ - background-color: #fff; - color: #333; - width: 300px; - overflow: auto; -} - -.TreeNode, .TreeItem -{ - white-space: nowrap; - margin: 2px 2px 2px 2px; -} - -.TOCDiv -{ - position: relative; - float: left; - width: 300px; - height: 100%; -} - -.TOCSizer -{ - clear: none; - float: left; - width: 10px; - height: 100%; - background-image: url("Splitter.gif"); - background-position:center center; - background-repeat:no-repeat; - position: relative; - cursor: w-resize; - border-left: solid 1px #CCC; -} - -.TopicContent -{ - position: relative; - float: right; - background-color: white; - height: 100%; -} - -.SearchOpts -{ - padding: 5px 5px 10px 5px; - color: black; - width: 300px; - border-bottom: solid lightgrey 1px; - height: 110px !important; -} - -.NavOpts -{ - padding: 5px 5px 6px 5px; - color: black; - width: 300px; - border-bottom: solid lightgrey 1px; -} - -.NavOpts img { - display:inline-block; - margin: 0px 5px 0px 5px; -} - -.IndexOpts -{ - padding: 5px 5px 6px 5px; - color: black; - width: 300px; - border-bottom: solid lightgrey 1px; -} - -.IndexItem -{ - white-space: nowrap; - margin: 2px 2px 2px 2px; -} - -.IndexSubItem -{ - white-space: nowrap; - margin: 2px 2px 2px 12px; -} - -.PaddedText -{ - margin: 10px 10px 10px 10px; -} diff --git a/build/ApiDocs/csharp-api-docs.shfbproj b/build/ApiDocs/csharp-api-docs.shfbproj deleted file mode 100644 index f635f2e2ee..0000000000 --- a/build/ApiDocs/csharp-api-docs.shfbproj +++ /dev/null @@ -1,172 +0,0 @@ - - - - - Debug - AnyCPU - 2.0 - {cb4d85f1-7390-40ee-b41f-a724bb8fddea} - 1.9.5.0 - - Documentation - Documentation - Documentation - - .NET Framework 4.5 - .\Output\ - UmbracoClassLibrary - en-US - OnlyErrors - csharp-api-docs.log - Website - True - False - False - False - True - CSharp - Blank - False - VS2010 - False - Guid - Umbraco .Net Class Library - AboveNamespaces - Attributes, InheritedMembers, InheritedFrameworkMembers, Protected, SealedProtected - - - - - - - None - None - False - True - Summary, AutoDocumentCtors, AutoDocumentDispose - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build/Build.bat b/build/Build.bat index bd18f719ad..1167d9fcb6 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -21,14 +21,6 @@ ECHO Building Umbraco %version% ReplaceIISExpressPortNumber.exe ..\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj %release% -ECHO Installing the Microsoft.Bcl.Build package before anything else, otherwise you'd have to run build.cmd twice -SET nuGetFolder=%CD%\..\src\packages\ -..\src\.nuget\NuGet.exe sources Remove -Name MyGetUmbracoCore >NUL -..\src\.nuget\NuGet.exe sources Add -Name MyGetUmbracoCore -Source https://www.myget.org/F/umbracocore/api/v2/ >NUL -..\src\.nuget\NuGet.exe install ..\src\Umbraco.Web.UI\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet -..\src\.nuget\NuGet.exe install ..\src\umbraco.businesslogic\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet -..\src\.nuget\NuGet.exe install ..\src\Umbraco.Core\packages.config -OutputDirectory %nuGetFolder% -Verbosity quiet - ECHO Removing the belle build folder and bower_components folder to make sure everything is clean as a whistle RD ..\src\Umbraco.Web.UI.Client\build /Q /S RD ..\src\Umbraco.Web.UI.Client\bower_components /Q /S diff --git a/build/Build.proj b/build/Build.proj index e937df15bd..9413b49d15 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -175,8 +175,8 @@ diff --git a/build/BuildBelle.bat b/build/BuildBelle.bat index 0d58204f65..6c11cc9fc5 100644 --- a/build/BuildBelle.bat +++ b/build/BuildBelle.bat @@ -1,4 +1,6 @@ @ECHO OFF +SETLOCAL + SET release=%1 ECHO Installing Npm NuGet Package @@ -11,12 +13,9 @@ ECHO Current folder: %CD% for /f "delims=" %%A in ('dir %nuGetFolder%node.js.* /b') do set "nodePath=%nuGetFolder%%%A\" for /f "delims=" %%A in ('dir %nuGetFolder%npm.js.* /b') do set "npmPath=%nuGetFolder%%%A\tools\" -ECHO Temporarily adding Npm and Node to path -SET oldPath=%PATH% - -path=%npmPath%;%nodePath%;%PATH% - -ECHO %path% +ECHO Adding Npm and Node to path +REM SETLOCAL is on, so changes to the path not persist to the actual user's path +PATH=%npmPath%;%nodePath%;%PATH% SET buildFolder=%CD% @@ -29,8 +28,5 @@ call npm install -g grunt-cli --quiet call npm install -g bower --quiet call grunt build --buildversion=%release% -ECHO Reset path to what it was before -path=%oldPath% - ECHO Move back to the build folder CD %buildFolder% \ No newline at end of file diff --git a/build/BuildDocs.bat b/build/BuildDocs.bat new file mode 100644 index 0000000000..9d0a04e1cd --- /dev/null +++ b/build/BuildDocs.bat @@ -0,0 +1,20 @@ +@ECHO OFF +SETLOCAL + +SET release=%1 +ECHO Installing Npm NuGet Package + +SET nuGetFolder=%CD%\..\src\packages\ +ECHO Configured packages folder: %nuGetFolder% +ECHO Current folder: %CD% + +%CD%\..\src\.nuget\NuGet.exe install Npm.js -OutputDirectory %nuGetFolder% -Verbosity quiet + +for /f "delims=" %%A in ('dir %nuGetFolder%node.js.* /b') do set "nodePath=%nuGetFolder%%%A\" +for /f "delims=" %%A in ('dir %nuGetFolder%npm.js.* /b') do set "npmPath=%nuGetFolder%%%A\tools\" + +ECHO Adding Npm and Node to path +REM SETLOCAL is on, so changes to the path not persist to the actual user's path +PATH=%npmPath%;%nodePath%;%PATH% + +Powershell.exe -ExecutionPolicy Unrestricted -File .\BuildDocs.ps1 \ No newline at end of file diff --git a/build/BuildDocs.ps1 b/build/BuildDocs.ps1 index 6f46a43fde..dcb3a85cc1 100644 --- a/build/BuildDocs.ps1 +++ b/build/BuildDocs.ps1 @@ -1,27 +1,100 @@ -##We cannot continue if sandcastle is not installed determined by env variable: SHFBROOT +$PSScriptFilePath = (Get-Item $MyInvocation.MyCommand.Path); +$RepoRoot = (get-item $PSScriptFilePath).Directory.Parent.FullName; +$SolutionRoot = Join-Path -Path $RepoRoot "src"; +$ToolsRoot = Join-Path -Path $RepoRoot "tools"; +$DocFx = Join-Path -Path $ToolsRoot "docfx\docfx.exe" +$DocFxFolder = (Join-Path -Path $ToolsRoot "docfx") +$DocFxJson = Join-Path -Path $RepoRoot "apidocs\docfx.json" +$7Zip = Join-Path -Path $ToolsRoot "7zip\7za.exe" +$DocFxSiteOutput = Join-Path -Path $RepoRoot "apidocs\_site\*.*" +$NgDocsSiteOutput = Join-Path -Path $RepoRoot "src\Umbraco.Web.UI.Client\docs\api\*.*" +$ProgFiles86 = [Environment]::GetEnvironmentVariable("ProgramFiles(x86)"); +$MSBuild = "$ProgFiles86\MSBuild\14.0\Bin\MSBuild.exe" -if (-not (Test-Path Env:\SHFBROOT)) -{ - throw "The docs cannot be build, install Sandcastle help file builder" + +################ Do the UI docs + +"Changing to Umbraco.Web.UI.Client folder" +cd .. +cd src\Umbraco.Web.UI.Client +Write-Host $(Get-Location) + +"Creating build folder so MSBuild doesn't run the whole grunt build" +if (-Not (Test-Path "build")) { + md "build" } -$PSScriptFilePath = (Get-Item $MyInvocation.MyCommand.Path).FullName -$BuildRoot = Split-Path -Path $PSScriptFilePath -Parent -$OutputPath = Join-Path -Path $BuildRoot -ChildPath "ApiDocs\Output" -$ProjFile = Join-Path -Path $BuildRoot -ChildPath "ApiDocs\csharp-api-docs.shfbproj" +"Installing node" +# Check if Install-Product exists, should only exist on the build server +if (Get-Command Install-Product -errorAction SilentlyContinue) +{ + Install-Product node '' +} -"Building docs with project file: $ProjFile" +"Installing node modules" +& npm install -$MSBuild = "$Env:SYSTEMROOT\Microsoft.NET\Framework\v4.0.30319\msbuild.exe" +"Installing grunt" +& npm install -g grunt-cli -# build it! -& $MSBuild "$ProjFile" +"Moving back to build folder" +cd .. +cd .. +cd build +Write-Host $(Get-Location) -# remove files left over -Remove-Item $BuildRoot\* -include csharp-api-docs.shfbproj_* + & grunt --gruntfile ../src/umbraco.web.ui.client/gruntfile.js docs -# copy our custom styles in -Copy-Item $BuildRoot\ApiDocs\TOC.css $OutputPath\TOC.css +# change baseUrl +$BaseUrl = "https://our.umbraco.org/apidocs/ui/" +$IndexPath = "../src/umbraco.web.ui.client/docs/api/index.html" +(Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath +# zip it -"" -"Done!" \ No newline at end of file +& $7Zip a -tzip ui-docs.zip $NgDocsSiteOutput -r + +################ Do the c# docs + +# Build the solution in debug mode +$SolutionPath = Join-Path -Path $SolutionRoot -ChildPath "umbraco.sln" +& $MSBuild "$SolutionPath" /p:Configuration=Debug /maxcpucount /t:Clean +if (-not $?) +{ + throw "The MSBuild process returned an error code." +} +& $MSBuild "$SolutionPath" /p:Configuration=Debug /maxcpucount +if (-not $?) +{ + throw "The MSBuild process returned an error code." +} + +# Go get docfx if we don't hae it +$FileExists = Test-Path $DocFx +If ($FileExists -eq $False) { + + If(!(Test-Path $DocFxFolder)) + { + New-Item $DocFxFolder -type directory + } + + $DocFxZip = Join-Path -Path $ToolsRoot "docfx\docfx.zip" + $DocFxSource = "https://github.com/dotnet/docfx/releases/download/v1.9.4/docfx.zip" + Invoke-WebRequest $DocFxSource -OutFile $DocFxZip + + #unzip it + & $7Zip e $DocFxZip "-o$DocFxFolder" +} + +#clear site +If(Test-Path(Join-Path -Path $RepoRoot "apidocs\_site")) +{ + Remove-Item $DocFxSiteOutput -recurse +} + +# run it! +& $DocFx metadata $DocFxJson +& $DocFx build $DocFxJson + +# zip it + +& $7Zip a -tzip csharp-docs.zip $DocFxSiteOutput -r diff --git a/build/InstallGit.cmd b/build/InstallGit.cmd index f10fe6c100..b6ba71df9b 100644 --- a/build/InstallGit.cmd +++ b/build/InstallGit.cmd @@ -1,19 +1,19 @@ @ECHO OFF -SET oldPath=%PATH% +SETLOCAL +REM SETLOCAL is on, so changes to the path not persist to the actual user's path git.exe 2> NUL if %ERRORLEVEL%==9009 GOTO :trydefaultpath GOTO :EOF :trydefaultpath -path=C:\Program Files (x86)\Git\cmd;%PATH% +path=C:\Program Files (x86)\Git\cmd;C:\Program Files\Git\cmd;%PATH% git.exe 2> NUL if %ERRORLEVEL%==9009 GOTO :showerror GOTO :EOF :showerror -path=%oldPath% -ECHO Git is not in your path and could not be found in C:\Program Files (x86)\Git\bin +ECHO Git is not in your path and could not be found in C:\Program Files (x86)\Git\cmd nor in C:\Program Files\Git\cmd set /p install=" Do you want to install Git through Chocolatey [y/n]? " %=% if %install%==y ( GOTO :installgit @@ -29,5 +29,4 @@ GOTO :EOF ECHO Installing Chocolatey first @powershell -NoProfile -ExecutionPolicy unrestricted -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin ECHO Installing Git through Chocolatey -choco install git -path=C:\Program Files (x86)\Git\cmd;%path% +choco install git \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 0c7d51334e..72733c5800 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -17,27 +17,26 @@ - - + - + - + - - + + - - + + @@ -72,35 +71,34 @@ - - - + + - + - + - + - + - + - + - + - + - + - + - + - - + + \ No newline at end of file diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 61a8188804..66e82ac488 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -16,7 +16,8 @@ umbraco - + + diff --git a/build/NuSpecs/build/UmbracoCms.targets b/build/NuSpecs/build/UmbracoCms.targets index 024d8af7ad..fde5b4ea81 100644 --- a/build/NuSpecs/build/UmbracoCms.targets +++ b/build/NuSpecs/build/UmbracoCms.targets @@ -1,6 +1,17 @@  + + + $(MSBuildThisFileDirectory)..\UmbracoFiles\ + + + + + + + + $(MSBuildThisFileDirectory)..\UmbracoFiles\ @@ -47,4 +58,4 @@ - \ No newline at end of file + diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt index e4d621f693..a77632926c 100644 --- a/build/NuSpecs/tools/Dashboard.config.install.xdt +++ b/build/NuSpecs/tools/Dashboard.config.install.xdt @@ -1,6 +1,6 @@ -
+
@@ -20,7 +20,7 @@
-
+
views/dashboard/developer/developerdashboardvideos.html @@ -29,7 +29,7 @@
-
+
views/dashboard/developer/examinemanagement.html @@ -39,10 +39,10 @@ views/dashboard/developer/xmldataintegrityreport.html - +
-
+
@@ -52,7 +52,7 @@
-
+
@@ -62,17 +62,10 @@ views/dashboard/default/startupdashboardintro.html - - - - - views/dashboard/ChangePassword.html - -
-
+
@@ -84,4 +77,8 @@
+ +
+ +
\ No newline at end of file diff --git a/build/NuSpecs/tools/Readme.txt b/build/NuSpecs/tools/Readme.txt index 53d9c3c7da..b6b55c1c4f 100644 --- a/build/NuSpecs/tools/Readme.txt +++ b/build/NuSpecs/tools/Readme.txt @@ -10,6 +10,7 @@ Don't forget to build! + When upgrading your website using NuGet you should answer "No" to the questions to overwrite the Web.config file (and config files in the config folder). diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index 29aa41b55a..e0d660a795 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -10,8 +10,6 @@ Don't forget to build! -When upgrading your website using NuGet you should answer "No" to the questions to overwrite the Web.config -file (and config files in the config folder). We've done our best to transform your configuration files but in case something is not quite right: remember we backed up your files in App_Data\NuGetBackup so you can find the original files before they were transformed. diff --git a/build/NuSpecs/tools/Views.Web.config.install.xdt b/build/NuSpecs/tools/Views.Web.config.install.xdt index c34963f2b0..a5797232b8 100644 --- a/build/NuSpecs/tools/Views.Web.config.install.xdt +++ b/build/NuSpecs/tools/Views.Web.config.install.xdt @@ -17,6 +17,11 @@ + + + + + diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index f7fbad09bd..dfb9925840 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -7,8 +7,8 @@
-
- +
+
@@ -21,8 +21,10 @@ + + - + @@ -37,7 +39,7 @@ - + @@ -48,8 +50,15 @@ + + + + + - + + + > @@ -58,9 +67,9 @@ - + + @@ -72,13 +81,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -113,6 +280,8 @@ + + @@ -131,20 +300,27 @@ - - + + + + + + + - + + + @@ -156,6 +332,10 @@ + + + + @@ -166,29 +346,33 @@ - + + + + + - + - + - - + + - - + + - - + + @@ -196,19 +380,19 @@ - + - + - + - + - + \ No newline at end of file diff --git a/build/NuSpecs/tools/install.core.ps1 b/build/NuSpecs/tools/install.core.ps1 index 1fa9352be7..c4f213ba01 100644 --- a/build/NuSpecs/tools/install.core.ps1 +++ b/build/NuSpecs/tools/install.core.ps1 @@ -1,18 +1,30 @@ -param($rootPath, $toolsPath, $package, $project) +param($installPath, $toolsPath, $package, $project) + +Write-Host "installPath:" "${installPath}" +Write-Host "toolsPath:" "${toolsPath}" + +Write-Host " " if ($project) { $dateTime = Get-Date -Format yyyyMMdd-HHmmss - $backupPath = Join-Path (Split-Path $project.FullName -Parent) "\App_Data\NuGetBackup\$dateTime" + + # Create paths and list them + $projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName + Write-Host "projectPath:" "${projectPath}" + $backupPath = Join-Path $projectPath "App_Data\NuGetBackup\$dateTime" + Write-Host "backupPath:" "${backupPath}" $copyLogsPath = Join-Path $backupPath "CopyLogs" - $projectDestinationPath = Split-Path $project.FullName -Parent - + Write-Host "copyLogsPath:" "${copyLogsPath}" + $umbracoBinFolder = Join-Path $projectPath "bin" + Write-Host "umbracoBinFolder:" "${umbracoBinFolder}" + # Create backup folder and logs folder if it doesn't exist yet New-Item -ItemType Directory -Force -Path $backupPath New-Item -ItemType Directory -Force -Path $copyLogsPath # After backing up, remove all umbraco dlls from bin folder in case dll files are included in the VS project # See: http://issues.umbraco.org/issue/U4-4930 - $umbracoBinFolder = Join-Path $projectDestinationPath "bin" + if(Test-Path $umbracoBinFolder) { $umbracoBinBackupPath = Join-Path $backupPath "bin" @@ -20,7 +32,7 @@ if ($project) { robocopy $umbracoBinFolder $umbracoBinBackupPath /e /LOG:$copyLogsPath\UmbracoBinBackup.log - # Delete files Umbraco brings in + # Delete files Umbraco ships with if(Test-Path $umbracoBinFolder\businesslogic.dll) { Remove-Item $umbracoBinFolder\businesslogic.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\cms.dll) { Remove-Item $umbracoBinFolder\cms.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\controls.dll) { Remove-Item $umbracoBinFolder\controls.dll -Force -Confirm:$false } @@ -36,16 +48,18 @@ if ($project) { if(Test-Path $umbracoBinFolder\umbraco.DataLayer.dll) { Remove-Item $umbracoBinFolder\umbraco.DataLayer.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\umbraco.editorControls.dll) { Remove-Item $umbracoBinFolder\umbraco.editorControls.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\umbraco.MacroEngines.dll) { Remove-Item $umbracoBinFolder\umbraco.MacroEngines.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Umbraco.ModelsBuilder.dll) { Remove-Item $umbracoBinFolder\Umbraco.ModelsBuilder.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Umbraco.ModelsBuilder.AspNet.dll) { Remove-Item $umbracoBinFolder\Umbraco.ModelsBuilder.AspNet.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\umbraco.providers.dll) { Remove-Item $umbracoBinFolder\umbraco.providers.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Umbraco.Web.UI.dll) { Remove-Item $umbracoBinFolder\Umbraco.Web.UI.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\UmbracoExamine.dll) { Remove-Item $umbracoBinFolder\UmbracoExamine.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\UrlRewritingNet.UrlRewriter.dll) { Remove-Item $umbracoBinFolder\UrlRewritingNet.UrlRewriter.dll -Force -Confirm:$false } # Delete files Umbraco depends upon - $amd64Folder = Join-Path $projectDestinationPath "bin\amd64" + $amd64Folder = Join-Path $umbracoBinFolder "amd64" if(Test-Path $amd64Folder) { Remove-Item $amd64Folder -Force -Recurse -Confirm:$false } - $x86Folder = Join-Path $projectDestinationPath "bin\x86" - if(Test-Path $x86Folder) { Remove-Item $x86Folder -Force -Recurse -Confirm:$false } + $x86Folder = Join-Path $umbracoBinFolder "x86" + if(Test-Path $x86Folder) { Remove-Item $x86Folder -Force -Recurse -Confirm:$false } if(Test-Path $umbracoBinFolder\AutoMapper.dll) { Remove-Item $umbracoBinFolder\AutoMapper.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\AutoMapper.Net4.dll) { Remove-Item $umbracoBinFolder\AutoMapper.Net4.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\ClientDependency.Core.dll) { Remove-Item $umbracoBinFolder\ClientDependency.Core.dll -Force -Confirm:$false } @@ -57,19 +71,35 @@ if ($project) { if(Test-Path $umbracoBinFolder\ImageProcessor.dll) { Remove-Item $umbracoBinFolder\ImageProcessor.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\ImageProcessor.Web.dll) { Remove-Item $umbracoBinFolder\ImageProcessor.Web.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Lucene.Net.dll) { Remove-Item $umbracoBinFolder\Lucene.Net.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.AspNet.Identity.Core.dll) { Remove-Item $umbracoBinFolder\Microsoft.AspNet.Identity.Core.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.AspNet.Identity.Owin.dll) { Remove-Item $umbracoBinFolder\Microsoft.AspNet.Identity.Owin.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.CodeAnalysis.CSharp.dll) { Remove-Item $umbracoBinFolder\Microsoft.CodeAnalysis.CSharp.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.CodeAnalysis.dll) { Remove-Item $umbracoBinFolder\Microsoft.CodeAnalysis.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.Owin.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.Owin.Host.SystemWeb.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Host.SystemWeb.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.Cookies.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.Cookies.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.OAuth.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.OAuth.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.Web.Infrastructure.dll) { Remove-Item $umbracoBinFolder\Microsoft.Web.Infrastructure.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.Web.Helpers.dll) { Remove-Item $umbracoBinFolder\Microsoft.Web.Helpers.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Microsoft.Web.Mvc.FixedDisplayModes.dll) { Remove-Item $umbracoBinFolder\Microsoft.Web.Mvc.FixedDisplayModes.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\MiniProfiler.dll) { Remove-Item $umbracoBinFolder\MiniProfiler.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\MySql.Data.dll) { Remove-Item $umbracoBinFolder\MySql.Data.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\Newtonsoft.Json.dll) { Remove-Item $umbracoBinFolder\Newtonsoft.Json.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Owin.dll) { Remove-Item $umbracoBinFolder\Owin.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\Semver.dll) { Remove-Item $umbracoBinFolder\Semver.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Collections.Immutable.dll) { Remove-Item $umbracoBinFolder\System.Collections.Immutable.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Reflection.Metadata.dll) { Remove-Item $umbracoBinFolder\System.Reflection.Metadata.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Net.Http.Extensions.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Extensions.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Net.Http.Formatting.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Formatting.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Net.Http.Primitives.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Primitives.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Web.Helpers.dll) { Remove-Item $umbracoBinFolder\System.Web.Helpers.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.Http.dll) { Remove-Item $umbracoBinFolder\System.Web.Http.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.Http.WebHost.dll) { Remove-Item $umbracoBinFolder\System.Web.Http.WebHost.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.Mvc.dll) { Remove-Item $umbracoBinFolder\System.Web.Mvc.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.Razor.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.WebPages.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.dll -Force -Confirm:$false } if(Test-Path $umbracoBinFolder\System.Web.WebPages.Deployment.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Deployment.dll -Force -Confirm:$false } - if(Test-Path $umbracoBinFolder\System.Web.WebPages.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Razor.dll -Force -Confirm:$false } + if(Test-Path $umbracoBinFolder\System.Web.WebPages.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Razor.dll -Force -Confirm:$false } } } \ No newline at end of file diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 49b49f6cfe..5ebaa6d02a 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -1,21 +1,33 @@ -param($rootPath, $toolsPath, $package, $project) +param($installPath, $toolsPath, $package, $project) + +Write-Host "installPath:" "${installPath}" +Write-Host "toolsPath:" "${toolsPath}" + +Write-Host " " if ($project) { $dateTime = Get-Date -Format yyyyMMdd-HHmmss - $backupPath = Join-Path (Split-Path $project.FullName -Parent) "\App_Data\NuGetBackup\$dateTime" + + # Create paths and list them + $projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName + Write-Host "projectPath:" "${projectPath}" + $backupPath = Join-Path $projectPath "App_Data\NuGetBackup\$dateTime" + Write-Host "backupPath:" "${backupPath}" $copyLogsPath = Join-Path $backupPath "CopyLogs" - $projectDestinationPath = Split-Path $project.FullName -Parent + Write-Host "copyLogsPath:" "${copyLogsPath}" + $webConfigSource = Join-Path $projectPath "Web.config" + Write-Host "webConfigSource:" "${webConfigSource}" + $configFolder = Join-Path $projectPath "Config" + Write-Host "configFolder:" "${configFolder}" # Create backup folder and logs folder if it doesn't exist yet New-Item -ItemType Directory -Force -Path $backupPath New-Item -ItemType Directory -Force -Path $copyLogsPath # Create a backup of original web.config - $webConfigSource = Join-Path $projectDestinationPath "Web.config" Copy-Item $webConfigSource $backupPath -Force - # Backup config files folder - $configFolder = Join-Path $projectDestinationPath "Config" + # Backup config files folder if(Test-Path $configFolder) { $umbracoBackupPath = Join-Path $backupPath "Config" New-Item -ItemType Directory -Force -Path $umbracoBackupPath @@ -24,32 +36,24 @@ if ($project) { } # Copy umbraco and umbraco_files from package to project folder - # This is only done when these folders already exist because we - # only want to do this for upgrades - $umbracoFolder = Join-Path $projectDestinationPath "Umbraco" - if(Test-Path $umbracoFolder) { - $umbracoFolderSource = Join-Path $rootPath "UmbracoFiles\Umbraco" - - $umbracoBackupPath = Join-Path $backupPath "Umbraco" - New-Item -ItemType Directory -Force -Path $umbracoBackupPath - - robocopy $umbracoFolder $umbracoBackupPath /e /LOG:$copyLogsPath\UmbracoBackup.log - robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log - } + $umbracoFolder = Join-Path $projectPath "Umbraco" + New-Item -ItemType Directory -Force -Path $umbracoFolder + $umbracoFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco" + $umbracoBackupPath = Join-Path $backupPath "Umbraco" + New-Item -ItemType Directory -Force -Path $umbracoBackupPath + robocopy $umbracoFolder $umbracoBackupPath /e /LOG:$copyLogsPath\UmbracoBackup.log + robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log - $umbracoClientFolder = Join-Path $projectDestinationPath "Umbraco_Client" - if(Test-Path $umbracoClientFolder) { - $umbracoClientFolderSource = Join-Path $rootPath "UmbracoFiles\Umbraco_Client" - - $umbracoClientBackupPath = Join-Path $backupPath "Umbraco_Client" - New-Item -ItemType Directory -Force -Path $umbracoClientBackupPath - - robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log - robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log - } + $umbracoClientFolder = Join-Path $projectPath "Umbraco_Client" + New-Item -ItemType Directory -Force -Path $umbracoClientFolder + $umbracoClientFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco_Client" + $umbracoClientBackupPath = Join-Path $backupPath "Umbraco_Client" + New-Item -ItemType Directory -Force -Path $umbracoClientBackupPath + robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log + robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log $copyWebconfig = $true - $destinationWebConfig = Join-Path $projectDestinationPath "Web.config" + $destinationWebConfig = Join-Path $projectPath "Web.config" if(Test-Path $destinationWebConfig) { @@ -71,11 +75,31 @@ if ($project) { if($copyWebconfig -eq $true) { - $packageWebConfigSource = Join-Path $rootPath "UmbracoFiles\Web.config" + $packageWebConfigSource = Join-Path $installPath "UmbracoFiles\Web.config" Copy-Item $packageWebConfigSource $destinationWebConfig -Force - } - $installFolder = Join-Path $projectDestinationPath "Install" + # Copy files that don't get automatically copied for Website projects + # We do this here, when copyWebconfig is true because we only want to do it for new installs + # If this is an upgrade then the files should already be there + $splashesSource = Join-Path $installPath "UmbracoFiles\Config\splashes\*.*" + $splashesDestination = Join-Path $projectPath "Config\splashes\" + New-Item $splashesDestination -Type directory + Copy-Item $splashesSource $splashesDestination -Force + + $sqlCe64Source = Join-Path $installPath "UmbracoFiles\bin\amd64\*" + $sqlCe64Destination = Join-Path $projectPath "bin\amd64\" + Copy-Item $sqlCe64Source $sqlCe64Destination -Force + + $sqlCex86Source = Join-Path $installPath "UmbracoFiles\bin\x86\*" + $sqlCex86Destination = Join-Path $projectPath "bin\x86\" + Copy-Item $sqlCex86source $sqlCex86Destination -Force + + $umbracoUIXMLSource = Join-Path $installPath "UmbracoFiles\Umbraco\Config\Create\UI.xml" + $umbracoUIXMLDestination = Join-Path $projectPath "Umbraco\Config\Create\UI.xml" + Copy-Item $umbracoUIXMLSource $umbracoUIXMLDestination -Force + } + + $installFolder = Join-Path $projectPath "Install" if(Test-Path $installFolder) { Remove-Item $installFolder -Force -Recurse -Confirm:$false } diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index 45f43206fe..580c619547 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -1,36 +1,127 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/RevertToCleanInstall.bat b/build/RevertToCleanInstall.bat index 6c391386cd..b21b33d8ff 100644 --- a/build/RevertToCleanInstall.bat +++ b/build/RevertToCleanInstall.bat @@ -45,9 +45,6 @@ del ..\src\Umbraco.Web.UI\UserControls\*.* echo Removing masterpage files del ..\src\Umbraco.Web.UI\masterpages\*.* -echo Removing view files -del ..\src\Umbraco.Web.UI\Views\*.* - echo Removing razor files del ..\src\Umbraco.Web.UI\macroScripts\*.* @@ -104,7 +101,9 @@ echo Removing user control files FOR %%A IN (..\src\Umbraco.Web.UI\usercontrols\*.*) DO DEL %%A echo Removing view files -FOR %%A IN (..\src\Umbraco.Web.UI\Views\*.*) DO DEL %%A +ATTRIB +H ..\src\Umbraco.Web.UI\Views\Partials\Grid\*.cshtml /S +FOR %%A IN (..\src\Umbraco.Web.UI\Views\) DO DEL /Q /S *.cshtml -H +ATTRIB -H ..\src\Umbraco.Web.UI\Views\Partials\Grid\*.cshtml /S echo Removing razor files FOR %%A IN (..\src\Umbraco.Web.UI\macroScripts\*.*) DO DEL %%A diff --git a/build/RevertToEmptyInstall.bat b/build/RevertToEmptyInstall.bat index d59e2cdc88..b8abe4e64e 100644 --- a/build/RevertToEmptyInstall.bat +++ b/build/RevertToEmptyInstall.bat @@ -54,9 +54,6 @@ del ..\src\Umbraco.Web.UI\UserControls\*.* echo Removing masterpage files del ..\src\Umbraco.Web.UI\masterpages\*.* -echo Removing view files -del ..\src\Umbraco.Web.UI\Views\*.* - echo Removing razor files del ..\src\Umbraco.Web.UI\macroScripts\*.* @@ -122,7 +119,9 @@ echo Removing user control files FOR %%A IN (..\src\Umbraco.Web.UI\usercontrols\*.*) DO DEL %%A echo Removing view files -FOR %%A IN (..\src\Umbraco.Web.UI\Views\*.*) DO DEL %%A +ATTRIB +H ..\src\Umbraco.Web.UI\Views\Partials\Grid\*.cshtml /S +FOR %%A IN (..\src\Umbraco.Web.UI\Views\) DO DEL /Q /S *.cshtml -H +ATTRIB -H ..\src\Umbraco.Web.UI\Views\Partials\Grid\*.cshtml /S echo Removing razor files FOR %%A IN (..\src\Umbraco.Web.UI\macroScripts\*.*) DO DEL %%A @@ -151,6 +150,9 @@ rmdir "..\src\Umbraco.Web.UI\usercontrols\umbracoContour\" /S /Q echo Start with a clean web.config copy ..\src\Umbraco.Web.UI\web.Template.config ..\src\Umbraco.Web.UI\web.config /Y +echo Start with a clean web.config +copy ..\src\Umbraco.Web.UI\web.Template.config ..\src\Umbraco.Web.UI\web.config /Y + echo "Umbraco install reverted to clean install" pause exit diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index ed2ae80c6e..4b66ecc866 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.3.0 \ No newline at end of file +7.5.0 \ No newline at end of file diff --git a/src/NuGet.Config b/src/NuGet.Config new file mode 100644 index 0000000000..dcccfdd5c1 --- /dev/null +++ b/src/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index 73983e7e30..86796e1dd7 100644 --- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj +++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj @@ -47,13 +47,15 @@ - - True - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.dll + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll + False + False - - True - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.Entity.dll + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll + False + False diff --git a/src/SQLCE4Umbraco/packages.config b/src/SQLCE4Umbraco/packages.config index 5d2d5789e7..e2435f3e8b 100644 --- a/src/SQLCE4Umbraco/packages.config +++ b/src/SQLCE4Umbraco/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 6c766f8e97..caeb303f68 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2015")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.3.0")] -[assembly: AssemblyInformationalVersion("7.3.0")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.5.0")] +[assembly: AssemblyInformationalVersion("7.5.0")] \ No newline at end of file diff --git a/src/Umbraco.Core/ActionsResolver.cs b/src/Umbraco.Core/ActionsResolver.cs index ff34f62c60..2da95a3416 100644 --- a/src/Umbraco.Core/ActionsResolver.cs +++ b/src/Umbraco.Core/ActionsResolver.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core /// /// A resolver to return all IAction objects /// - internal sealed class ActionsResolver : LazyManyObjectsResolverBase + public sealed class ActionsResolver : LazyManyObjectsResolverBase { /// /// Constructor diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 4aef011d05..e47ef04650 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -283,7 +283,12 @@ namespace Umbraco.Core { var configStatus = ConfigurationStatus; var currentVersion = UmbracoVersion.GetSemanticVersion(); - var ok = configStatus == currentVersion; + + var ok = + //we are not configured if this is null + string.IsNullOrWhiteSpace(configStatus) == false + //they must match + && configStatus == currentVersion; if (ok) { @@ -308,8 +313,9 @@ namespace Umbraco.Core return ok; } - catch + catch (Exception ex) { + LogHelper.Error("Error determining if application is configured, returning false", ex); return false; } @@ -400,7 +406,8 @@ namespace Umbraco.Core //clear the cache if (ApplicationCache != null) { - ApplicationCache.ClearAllCache(); + ApplicationCache.RuntimeCache.ClearAllCache(); + ApplicationCache.IsolatedRuntimeCache.ClearAllCaches(); } //reset all resolvers ResolverCollection.ResetAll(); diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs index b4e8e64312..b6a3f8534e 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/AsyncLock.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -117,38 +118,55 @@ namespace Umbraco.Core private class NamedSemaphoreReleaser : CriticalFinalizerObject, IDisposable { private readonly Semaphore _semaphore; + private GCHandle _handle; internal NamedSemaphoreReleaser(Semaphore semaphore) { _semaphore = semaphore; + _handle = GCHandle.Alloc(_semaphore); } public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); + GC.SuppressFinalize(this); // finalize will not run } private void Dispose(bool disposing) { // critical + _handle.Free(); _semaphore.Release(); + _semaphore.Dispose(); } - // we WANT to release the semaphore because it's a system object - // ie a critical non-managed resource - so we inherit from CriticalFinalizerObject - // which means that the finalizer "should" run in all situations + // we WANT to release the semaphore because it's a system object, ie a critical + // non-managed resource - and if it is not released then noone else can acquire + // the lock - so we inherit from CriticalFinalizerObject which means that the + // finalizer "should" run in all situations - there is always a chance that it + // does not run and the semaphore remains "acquired" but then chances are the + // whole process (w3wp.exe...) is going down, at which point the semaphore will + // be destroyed by Windows. - // however... that can fail with System.ObjectDisposedException because the - // underlying handle was closed... because we cannot guarantee that the semaphore - // is not gone already... unless we get a GCHandle = GCHandle.Alloc(_semaphore); - // which should keep it around and then we free the handle? + // however, the semaphore is a managed object, and so when the finalizer runs it + // might have been finalized already, and then we get a, ObjectDisposedException + // in the finalizer - which is bad. - // so... I'm not sure this is safe really... + // in order to prevent this we do two things + // - use a GCHandler to ensure the semaphore is still there when the finalizer + // runs, so we can actually release it + // - wrap the finalizer code in a try...catch to make sure it never throws ~NamedSemaphoreReleaser() { - Dispose(false); + try + { + Dispose(false); + } + catch + { + // we do NOT want the finalizer to throw - never ever + } } } diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 88d570beff..0c1a202b66 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -1,8 +1,9 @@ using System; +using System.ComponentModel; +using Umbraco.Core.CodeAnnotations; namespace Umbraco.Core.Cache { - /// /// Constants storing cache keys used in caching /// @@ -12,52 +13,78 @@ namespace Umbraco.Core.Cache public const string ApplicationsCacheKey = "ApplicationCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserTypeCacheKey = "UserTypeCache"; + [Obsolete("This is no longer used and will be removed from the codebase in the future - it is referenced but no cache is stored against this key")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string ContentItemCacheKey = "contentItem"; + [UmbracoWillObsolete("This cache key is only used for the legacy 'library' caching, remove in v8")] public const string MediaCacheKey = "UL_GetMedia"; public const string MacroXsltCacheKey = "macroXslt_"; + + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string MacroCacheKey = "UmbracoMacroCache"; + public const string MacroHtmlCacheKey = "macroHtml_"; public const string MacroControlCacheKey = "macroControl_"; public const string MacroHtmlDateAddedCacheKey = "macroHtml_DateAdded_"; public const string MacroControlDateAddedCacheKey = "macroControl_DateAdded_"; + [UmbracoWillObsolete("This cache key is only used for legacy 'library' member caching, remove in v8")] public const string MemberLibraryCacheKey = "UL_GetMember"; + + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string MemberBusinessLogicCacheKey = "MemberCacheItem_"; - + + [UmbracoWillObsolete("This cache key is only used for legacy template business logic caching, remove in v8")] public const string TemplateFrontEndCacheKey = "template"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string TemplateBusinessLogicCacheKey = "UmbracoTemplateCache"; + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserContextCacheKey = "UmbracoUserContext"; + public const string UserContextTimeoutCacheKey = "UmbracoUserContextTimeout"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string UserCacheKey = "UmbracoUser"; public const string UserPermissionsCacheKey = "UmbracoUserPermissions"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypeCacheKey = "UmbracoContentType"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypePropertiesCacheKey = "ContentType_PropertyTypes_Content:"; + [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string PropertyTypeCacheKey = "UmbracoPropertyTypeCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string LanguageCacheKey = "UmbracoLanguageCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string DomainCacheKey = "UmbracoDomainList"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string StylesheetCacheKey = "UmbracoStylesheet"; + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string StylesheetPropertyCacheKey = "UmbracoStylesheetProperty"; + [Obsolete("This is no longer used and will be removed from the codebase in the future")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string DataTypeCacheKey = "UmbracoDataTypeDefinition"; public const string DataTypePreValuesCacheKey = "UmbracoPreVal"; diff --git a/src/Umbraco.Core/Cache/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/CacheRefresherBase.cs index 2931805b08..60ad69b6fc 100644 --- a/src/Umbraco.Core/Cache/CacheRefresherBase.cs +++ b/src/Umbraco.Core/Cache/CacheRefresherBase.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Sync; using umbraco.interfaces; +using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Cache { @@ -63,5 +64,15 @@ namespace Umbraco.Core.Cache { OnCacheUpdated(Instance, new CacheRefresherEventArgs(id, MessageType.RefreshById)); } + + /// + /// Clears the cache for all repository entities of this type + /// + /// + internal void ClearAllIsolatedCacheByEntityType() + where TEntity : class, IAggregateRoot + { + ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.ClearCache(); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DeepCloneRuntimeCacheProvider.cs b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs similarity index 93% rename from src/Umbraco.Core/Persistence/Repositories/DeepCloneRuntimeCacheProvider.cs rename to src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs index 0b5b42660d..0ae721943d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DeepCloneRuntimeCacheProvider.cs +++ b/src/Umbraco.Core/Cache/DeepCloneRuntimeCacheProvider.cs @@ -2,20 +2,27 @@ using System; using System.Collections.Generic; using System.Linq; using System.Web.Caching; -using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; -namespace Umbraco.Core.Persistence.Repositories +namespace Umbraco.Core.Cache { + /// + /// Interface describing this cache provider as a wrapper for another + /// + internal interface IRuntimeCacheProviderWrapper + { + IRuntimeCacheProvider InnerProvider { get; } + } + /// /// A wrapper for any IRuntimeCacheProvider that ensures that all inserts and returns /// are a deep cloned copy of the item when the item is IDeepCloneable and that tracks changes are /// reset if the object is TracksChangesEntityBase /// - internal class DeepCloneRuntimeCacheProvider : IRuntimeCacheProvider + internal class DeepCloneRuntimeCacheProvider : IRuntimeCacheProvider, IRuntimeCacheProviderWrapper { - internal IRuntimeCacheProvider InnerProvider { get; private set; } + public IRuntimeCacheProvider InnerProvider { get; private set; } public DeepCloneRuntimeCacheProvider(IRuntimeCacheProvider innerProvider) { diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs new file mode 100644 index 0000000000..1f51fc3ccc --- /dev/null +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// The default cache policy for retrieving a single entity + /// + /// + /// + /// + /// This cache policy uses sliding expiration and caches instances for 5 minutes. However if allow zero count is true, then we use the + /// default policy with no expiry. + /// + internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IAggregateRoot + { + private readonly RepositoryCachePolicyOptions _options; + + public DefaultRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) + : base(cache) + { + if (options == null) throw new ArgumentNullException("options"); + _options = options; + } + + protected string GetCacheIdKey(object id) + { + if (id == null) throw new ArgumentNullException("id"); + + return string.Format("{0}{1}", GetCacheTypeKey(), id); + } + + protected string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(TEntity).Name); + } + + public override void CreateOrUpdate(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + + //set the disposal action + SetCacheAction(() => + { + //just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) + { + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + + } + catch + { + //set the disposal action + SetCacheAction(() => + { + //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way + // that we cache entities: http://issues.umbraco.org/issue/U4-4259 + Cache.ClearCacheItem(GetCacheIdKey(entity.Id)); + + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + + throw; + } + } + + public override void Remove(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + } + finally + { + //set the disposal action + var cacheKey = GetCacheIdKey(entity.Id); + SetCacheAction(() => + { + Cache.ClearCacheItem(cacheKey); + //If there's a GetAllCacheAllowZeroCount cache, ensure it is cleared + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + } + + public override TEntity Get(TId id, Func getFromRepo) + { + if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + + var cacheKey = GetCacheIdKey(id); + var fromCache = Cache.GetCacheItem(cacheKey); + if (fromCache != null) + return fromCache; + + var entity = getFromRepo(id); + + //set the disposal action + SetCacheAction(cacheKey, entity); + + return entity; + } + + public override TEntity Get(TId id) + { + var cacheKey = GetCacheIdKey(id); + return Cache.GetCacheItem(cacheKey); + } + + public override bool Exists(TId id, Func getFromRepo) + { + if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + + var cacheKey = GetCacheIdKey(id); + var fromCache = Cache.GetCacheItem(cacheKey); + return fromCache != null || getFromRepo(id); + } + + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + if (getFromRepo == null) throw new ArgumentNullException("getFromRepo"); + + if (ids.Any()) + { + var entities = ids.Select(Get).ToArray(); + if (ids.Length.Equals(entities.Length) && entities.Any(x => x == null) == false) + return entities; + } + else + { + var allEntities = GetAllFromCache(); + if (allEntities.Any()) + { + if (_options.GetAllCacheValidateCount) + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + var totalCount = _options.PerformCount(); + if (allEntities.Length == totalCount) + return allEntities; + } + else + { + return allEntities; + } + } + else if (_options.GetAllCacheAllowZeroCount) + { + //if the repository allows caching a zero count, then check the zero count cache + if (HasZeroCountCache()) + { + //there is a zero count cache so return an empty list + return new TEntity[] {}; + } + } + } + + //we need to do the lookup from the repo + var entityCollection = getFromRepo(ids) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull() + .ToArray(); + + //set the disposal action + SetCacheAction(ids, entityCollection); + + return entityCollection; + } + + /// + /// Looks up the zero count cache, must return null if it doesn't exist + /// + /// + protected bool HasZeroCountCache() + { + var zeroCount = Cache.GetCacheItem(GetCacheTypeKey()); + return (zeroCount != null && zeroCount.Any() == false); + } + + /// + /// Performs the lookup for all entities of this type from the cache + /// + /// + protected TEntity[] GetAllFromCache() + { + var allEntities = Cache.GetCacheItemsByKeySearch(GetCacheTypeKey()) + .WhereNotNull() + .ToArray(); + return allEntities.Any() ? allEntities : new TEntity[] {}; + } + + /// + /// Sets the action to execute on disposal for a single entity + /// + /// + /// + protected virtual void SetCacheAction(string cacheKey, TEntity entity) + { + if (entity == null) return; + + SetCacheAction(() => + { + //just to be safe, we cannot cache an item without an identity + if (entity.HasIdentity) + { + Cache.InsertCacheItem(cacheKey, () => entity, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + }); + } + + /// + /// Sets the action to execute on disposal for an entity collection + /// + /// + /// + protected virtual void SetCacheAction(TId[] ids, TEntity[] entityCollection) + { + SetCacheAction(() => + { + //This option cannot execute if we are looking up specific Ids + if (ids.Any() == false && entityCollection.Length == 0 && _options.GetAllCacheAllowZeroCount) + { + //there was nothing returned but we want to cache a zero count result so add an TEntity[] to the cache + // to signify that there is a zero count cache + //NOTE: Don't set expiry/sliding for a zero count + Cache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); + } + else + { + //This is the default behavior, we'll individually cache each item so that if/when these items are resolved + // by id, they are returned from the already existing cache. + foreach (var entity in entityCollection.WhereNotNull()) + { + var localCopy = entity; + //just to be safe, we cannot cache an item without an identity + if (localCopy.HasIdentity) + { + Cache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy, + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + } + } + }); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..5c02e41a48 --- /dev/null +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicyFactory.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class DefaultRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly RepositoryCachePolicyOptions _options; + + public DefaultRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) + { + _runtimeCache = runtimeCache; + _options = options; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new DefaultRepositoryCachePolicy(_runtimeCache, _options); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs new file mode 100644 index 0000000000..9b37d1861f --- /dev/null +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicy.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Collections; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// A caching policy that caches an entire dataset as a single collection + /// + /// + /// + internal class FullDataSetRepositoryCachePolicy : RepositoryCachePolicyBase + where TEntity : class, IAggregateRoot + { + private readonly Func _getEntityId; + private readonly Func> _getAllFromRepo; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicy(IRuntimeCacheProvider cache, Func getEntityId, Func> getAllFromRepo, bool expires) + : base(cache) + { + _getEntityId = getEntityId; + _getAllFromRepo = getAllFromRepo; + _expires = expires; + } + + private bool? _hasZeroCountCache; + + + protected string GetCacheTypeKey() + { + return string.Format("uRepo_{0}_", typeof(TEntity).Name); + } + + public override void CreateOrUpdate(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + catch + { + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + throw; + } + } + + public override void Remove(TEntity entity, Action persistMethod) + { + if (entity == null) throw new ArgumentNullException("entity"); + if (persistMethod == null) throw new ArgumentNullException("persistMethod"); + + try + { + persistMethod(entity); + } + finally + { + //set the disposal action + SetCacheAction(() => + { + //Clear all + Cache.ClearCacheItem(GetCacheTypeKey()); + }); + } + } + + public override TEntity Get(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return from the repo + if (found == null) return getFromRepo(id); + var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + if (entity == null) return null; + + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + return (TEntity)entity.DeepClone(); + } + + public override TEntity Get(TId id) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return null + if (found == null) return null; + var entity = found.FirstOrDefault(x => _getEntityId(x).Equals(id)); + if (entity == null) return null; + + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + return (TEntity)entity.DeepClone(); + } + + public override bool Exists(TId id, Func getFromRepo) + { + //Force get all with cache + var found = GetAll(new TId[] { }, ids => _getAllFromRepo().WhereNotNull()); + + //we don't have anything in cache (this should never happen), just return from the repo + return found == null + ? getFromRepo(id) + : found.Any(x => _getEntityId(x).Equals(id)); + } + + public override TEntity[] GetAll(TId[] ids, Func> getFromRepo) + { + //process getting all including setting the cache callback + var result = PerformGetAll(getFromRepo); + + //now that the base result has been calculated, they will all be cached. + // Now we can just filter by ids if they have been supplied + + return (ids.Any() + ? result.Where(x => ids.Contains(_getEntityId(x))).ToArray() + : result) + //We must ensure to deep clone each one out manually since the deep clone list only clones one way + .Select(x => (TEntity)x.DeepClone()) + .ToArray(); + } + + private TEntity[] PerformGetAll(Func> getFromRepo) + { + var allEntities = GetAllFromCache(); + if (allEntities.Any()) + { + return allEntities; + } + + //check the zero count cache + if (HasZeroCountCache()) + { + //there is a zero count cache so return an empty list + return new TEntity[] { }; + } + + //we need to do the lookup from the repo + var entityCollection = getFromRepo(new TId[] { }) + //ensure we don't include any null refs in the returned collection! + .WhereNotNull() + .ToArray(); + + //set the disposal action + SetCacheAction(entityCollection); + + return entityCollection; + } + + /// + /// For this type of caching policy, we don't cache individual items + /// + /// + /// + protected void SetCacheAction(string cacheKey, TEntity entity) + { + //No-op + } + + /// + /// Sets the action to execute on disposal for an entity collection + /// + /// + protected void SetCacheAction(TEntity[] entityCollection) + { + //set the disposal action + SetCacheAction(() => + { + //We want to cache the result as a single collection + + if (_expires) + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection), + timeout: TimeSpan.FromMinutes(5), + isSliding: true); + } + else + { + Cache.InsertCacheItem(GetCacheTypeKey(), () => new DeepCloneableList(entityCollection)); + } + }); + } + + /// + /// Looks up the zero count cache, must return null if it doesn't exist + /// + /// + protected bool HasZeroCountCache() + { + if (_hasZeroCountCache.HasValue) + return _hasZeroCountCache.Value; + + _hasZeroCountCache = Cache.GetCacheItem>(GetCacheTypeKey()) != null; + return _hasZeroCountCache.Value; + } + + /// + /// This policy will cache the full data set as a single collection + /// + /// + protected TEntity[] GetAllFromCache() + { + var found = Cache.GetCacheItem>(GetCacheTypeKey()); + + //This method will get called before checking for zero count cache, so we'll just set the flag here + _hasZeroCountCache = found != null; + + return found == null ? new TEntity[] { } : found.WhereNotNull().ToArray(); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..e4addcf355 --- /dev/null +++ b/src/Umbraco.Core/Cache/FullDataSetRepositoryCachePolicyFactory.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class FullDataSetRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly Func _getEntityId; + private readonly Func> _getAllFromRepo; + private readonly bool _expires; + + public FullDataSetRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, Func getEntityId, Func> getAllFromRepo, bool expires) + { + _runtimeCache = runtimeCache; + _getEntityId = getEntityId; + _getAllFromRepo = getAllFromRepo; + _expires = expires; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new FullDataSetRepositoryCachePolicy(_runtimeCache, _getEntityId, _getAllFromRepo, _expires); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/ICacheProvider.cs b/src/Umbraco.Core/Cache/ICacheProvider.cs index 0d2bb1bdb6..e53123d7e8 100644 --- a/src/Umbraco.Core/Cache/ICacheProvider.cs +++ b/src/Umbraco.Core/Cache/ICacheProvider.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Cache { /// - /// An abstract class for implementing a basic cache provider + /// An interface for implementing a basic cache provider /// public interface ICacheProvider { @@ -65,4 +65,4 @@ namespace Umbraco.Core.Cache object GetCacheItem(string cacheKey, Func getCacheItem); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs new file mode 100644 index 0000000000..215487c3be --- /dev/null +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicy.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal interface IRepositoryCachePolicy : IDisposable + where TEntity : class, IAggregateRoot + { + TEntity Get(TId id, Func getFromRepo); + TEntity Get(TId id); + bool Exists(TId id, Func getFromRepo); + + void CreateOrUpdate(TEntity entity, Action persistMethod); + void Remove(TEntity entity, Action persistMethod); + TEntity[] GetAll(TId[] ids, Func> getFromRepo); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..2d69704b63 --- /dev/null +++ b/src/Umbraco.Core/Cache/IRepositoryCachePolicyFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal interface IRepositoryCachePolicyFactory where TEntity : class, IAggregateRoot + { + IRepositoryCachePolicy CreatePolicy(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/IsolatedRuntimeCache.cs b/src/Umbraco.Core/Cache/IsolatedRuntimeCache.cs new file mode 100644 index 0000000000..da20f7eb73 --- /dev/null +++ b/src/Umbraco.Core/Cache/IsolatedRuntimeCache.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Concurrent; + +namespace Umbraco.Core.Cache +{ + /// + /// Used to get/create/manipulate isolated runtime cache + /// + /// + /// This is useful for repository level caches to ensure that cache lookups by key are fast so + /// that the repository doesn't need to search through all keys on a global scale. + /// + public class IsolatedRuntimeCache + { + internal Func CacheFactory { get; set; } + + /// + /// Constructor that allows specifying a factory for the type of runtime isolated cache to create + /// + /// + public IsolatedRuntimeCache(Func cacheFactory) + { + CacheFactory = cacheFactory; + } + + private readonly ConcurrentDictionary _isolatedCache = new ConcurrentDictionary(); + + /// + /// Returns an isolated runtime cache for a given type + /// + /// + /// + public IRuntimeCacheProvider GetOrCreateCache() + { + return _isolatedCache.GetOrAdd(typeof(T), type => CacheFactory(type)); + } + + /// + /// Returns an isolated runtime cache for a given type + /// + /// + public IRuntimeCacheProvider GetOrCreateCache(Type type) + { + return _isolatedCache.GetOrAdd(type, t => CacheFactory(t)); + } + + /// + /// Tries to get a cache by the type specified + /// + /// + /// + public Attempt GetCache() + { + IRuntimeCacheProvider cache; + if (_isolatedCache.TryGetValue(typeof(T), out cache)) + { + return Attempt.Succeed(cache); + } + return Attempt.Fail(); + } + + /// + /// Clears all values inside this isolated runtime cache + /// + /// + /// + public void ClearCache() + { + IRuntimeCacheProvider cache; + if (_isolatedCache.TryGetValue(typeof(T), out cache)) + { + cache.ClearAllCache(); + } + } + + /// + /// Clears all of the isolated caches + /// + public void ClearAllCaches() + { + foreach (var key in _isolatedCache.Keys) + { + IRuntimeCacheProvider cache; + if (_isolatedCache.TryRemove(key, out cache)) + { + cache.ClearAllCache(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs new file mode 100644 index 0000000000..b24838bc3b --- /dev/null +++ b/src/Umbraco.Core/Cache/OnlySingleItemsRepositoryCachePolicyFactory.cs @@ -0,0 +1,27 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// Creates cache policies + /// + /// + /// + internal class OnlySingleItemsRepositoryCachePolicyFactory : IRepositoryCachePolicyFactory + where TEntity : class, IAggregateRoot + { + private readonly IRuntimeCacheProvider _runtimeCache; + private readonly RepositoryCachePolicyOptions _options; + + public OnlySingleItemsRepositoryCachePolicyFactory(IRuntimeCacheProvider runtimeCache, RepositoryCachePolicyOptions options) + { + _runtimeCache = runtimeCache; + _options = options; + } + + public virtual IRepositoryCachePolicy CreatePolicy() + { + return new SingleItemsOnlyRepositoryCachePolicy(_runtimeCache, _options); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs new file mode 100644 index 0000000000..b939cd14e6 --- /dev/null +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + internal abstract class RepositoryCachePolicyBase : DisposableObject, IRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + private Action _action; + + protected RepositoryCachePolicyBase(IRuntimeCacheProvider cache) + { + if (cache == null) throw new ArgumentNullException("cache"); + + Cache = cache; + } + + protected IRuntimeCacheProvider Cache { get; private set; } + + /// + /// The disposal performs the caching + /// + protected override void DisposeResources() + { + if (_action != null) + { + _action(); + } + } + + /// + /// Sets the action to execute on disposal + /// + /// + protected void SetCacheAction(Action action) + { + _action = action; + } + + public abstract TEntity Get(TId id, Func getFromRepo); + public abstract TEntity Get(TId id); + public abstract bool Exists(TId id, Func getFromRepo); + public abstract void CreateOrUpdate(TEntity entity, Action persistMethod); + public abstract void Remove(TEntity entity, Action persistMethod); + public abstract TEntity[] GetAll(TId[] ids, Func> getFromRepo); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs similarity index 57% rename from src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs rename to src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs index 9ac8aa6abd..e8c6ac02b0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryCacheOptions.cs +++ b/src/Umbraco.Core/Cache/RepositoryCachePolicyOptions.cs @@ -1,17 +1,34 @@ -namespace Umbraco.Core.Persistence.Repositories +using System; + +namespace Umbraco.Core.Cache { - internal class RepositoryCacheOptions + internal class RepositoryCachePolicyOptions { /// - /// Constructor sets defaults + /// Ctor - sets GetAllCacheValidateCount = true /// - public RepositoryCacheOptions() + public RepositoryCachePolicyOptions(Func performCount) { + PerformCount = performCount; GetAllCacheValidateCount = true; GetAllCacheAllowZeroCount = false; - GetAllCacheThresholdLimit = 100; } + /// + /// Ctor - sets GetAllCacheValidateCount = false + /// + public RepositoryCachePolicyOptions() + { + PerformCount = null; + GetAllCacheValidateCount = false; + GetAllCacheAllowZeroCount = false; + } + + /// + /// Callback required to get count for GetAllCacheValidateCount + /// + public Func PerformCount { get; private set; } + /// /// True/false as to validate the total item count when all items are returned from cache, the default is true but this /// means that a db lookup will occur - though that lookup will probably be significantly less expensive than the normal @@ -21,16 +38,11 @@ namespace Umbraco.Core.Persistence.Repositories /// setting this to return false will improve performance of GetAll cache with no params but should only be used /// for specific circumstances /// - public bool GetAllCacheValidateCount { get; set; } + public bool GetAllCacheValidateCount { get; private set; } /// /// True if the GetAll method will cache that there are zero results so that the db is not hit when there are no results found /// public bool GetAllCacheAllowZeroCount { get; set; } - - /// - /// The threshold entity count for which the GetAll method will cache entities - /// - public int GetAllCacheThresholdLimit { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs new file mode 100644 index 0000000000..28ac4ee2d1 --- /dev/null +++ b/src/Umbraco.Core/Cache/SingleItemsOnlyRepositoryCachePolicy.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Umbraco.Core.Collections; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Cache +{ + /// + /// A caching policy that ignores all caches for GetAll - it will only cache calls for individual items + /// + /// + /// + internal class SingleItemsOnlyRepositoryCachePolicy : DefaultRepositoryCachePolicy + where TEntity : class, IAggregateRoot + { + public SingleItemsOnlyRepositoryCachePolicy(IRuntimeCacheProvider cache, RepositoryCachePolicyOptions options) : base(cache, options) + { + } + + protected override void SetCacheAction(TId[] ids, TEntity[] entityCollection) + { + //no-op + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/CacheHelper.cs b/src/Umbraco.Core/CacheHelper.cs index 51cf37aa23..0dc5f5b00f 100644 --- a/src/Umbraco.Core/CacheHelper.cs +++ b/src/Umbraco.Core/CacheHelper.cs @@ -1,6 +1,8 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -11,20 +13,15 @@ using Umbraco.Core.Logging; namespace Umbraco.Core { - /// /// Class that is exposed by the ApplicationContext for application wide caching purposes /// public class CacheHelper { - private readonly bool _enableCache; - private readonly ICacheProvider _requestCache; - private readonly ICacheProvider _nullRequestCache = new NullCacheProvider(); - private readonly ICacheProvider _staticCache; - private readonly ICacheProvider _nullStaticCache = new NullCacheProvider(); - private readonly IRuntimeCacheProvider _httpCache; - private readonly IRuntimeCacheProvider _nullHttpCache = new NullCacheProvider(); - + private static readonly ICacheProvider NullRequestCache = new NullCacheProvider(); + private static readonly ICacheProvider NullStaticCache = new NullCacheProvider(); + private static readonly IRuntimeCacheProvider NullRuntimeCache = new NullCacheProvider(); + /// /// Creates a cache helper with disabled caches /// @@ -34,7 +31,7 @@ namespace Umbraco.Core /// public static CacheHelper CreateDisabledCacheHelper() { - return new CacheHelper(null, null, null, false); + return new CacheHelper(NullRuntimeCache, NullStaticCache, NullRequestCache, new IsolatedRuntimeCache(t => NullRuntimeCache)); } /// @@ -44,7 +41,8 @@ namespace Umbraco.Core : this( new HttpRuntimeCacheProvider(HttpRuntime.Cache), new StaticCacheProvider(), - new HttpRequestCacheProvider()) + new HttpRequestCacheProvider(), + new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())) { } @@ -56,93 +54,75 @@ namespace Umbraco.Core : this( new HttpRuntimeCacheProvider(cache), new StaticCacheProvider(), - new HttpRequestCacheProvider()) + new HttpRequestCacheProvider(), + new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())) { } - /// - /// Initializes a new instance based on the provided providers - /// - /// - /// - /// + [Obsolete("Use the constructor the specifies all dependencies")] + [EditorBrowsable(EditorBrowsableState.Never)] public CacheHelper( IRuntimeCacheProvider httpCacheProvider, ICacheProvider staticCacheProvider, ICacheProvider requestCacheProvider) - : this(httpCacheProvider, staticCacheProvider, requestCacheProvider, true) + : this(httpCacheProvider, staticCacheProvider, requestCacheProvider, new IsolatedRuntimeCache(t => new ObjectCacheRuntimeCacheProvider())) { } - /// - /// Private ctor used for creating a disabled cache helper - /// - /// - /// - /// - /// - private CacheHelper( + /// + /// Initializes a new instance based on the provided providers + /// + /// + /// + /// + /// + public CacheHelper( IRuntimeCacheProvider httpCacheProvider, ICacheProvider staticCacheProvider, - ICacheProvider requestCacheProvider, - bool enableCache) + ICacheProvider requestCacheProvider, + IsolatedRuntimeCache isolatedCacheManager) { - if (enableCache) - { - _httpCache = httpCacheProvider; - _staticCache = staticCacheProvider; - _requestCache = requestCacheProvider; - } - else - { - _httpCache = null; - _staticCache = null; - _requestCache = null; - } - - _enableCache = enableCache; + if (httpCacheProvider == null) throw new ArgumentNullException("httpCacheProvider"); + if (staticCacheProvider == null) throw new ArgumentNullException("staticCacheProvider"); + if (requestCacheProvider == null) throw new ArgumentNullException("requestCacheProvider"); + if (isolatedCacheManager == null) throw new ArgumentNullException("isolatedCacheManager"); + RuntimeCache = httpCacheProvider; + StaticCache = staticCacheProvider; + RequestCache = requestCacheProvider; + IsolatedRuntimeCache = isolatedCacheManager; } /// /// Returns the current Request cache /// - public ICacheProvider RequestCache - { - get { return _enableCache ? _requestCache : _nullRequestCache; } - } - - /// - /// Returns the current Runtime cache - /// - public ICacheProvider StaticCache - { - get { return _enableCache ? _staticCache : _nullStaticCache; } - } - - /// - /// Returns the current Runtime cache - /// - public IRuntimeCacheProvider RuntimeCache - { - get { return _enableCache ? _httpCache : _nullHttpCache; } - } + public ICacheProvider RequestCache { get; internal set; } - #region Legacy Runtime/Http Cache accessors + /// + /// Returns the current Runtime cache + /// + public ICacheProvider StaticCache { get; internal set; } + + /// + /// Returns the current Runtime cache + /// + public IRuntimeCacheProvider RuntimeCache { get; internal set; } + + /// + /// Returns the current Isolated Runtime cache manager + /// + public IsolatedRuntimeCache IsolatedRuntimeCache { get; internal set; } + + #region Legacy Runtime/Http Cache accessors /// /// Clears the item in umbraco's runtime cache /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearAllCache() { - if (_enableCache == false) - { - _nullHttpCache.ClearAllCache(); - } - else - { - _httpCache.ClearAllCache(); - } + RuntimeCache.ClearAllCache(); + IsolatedRuntimeCache.ClearAllCaches(); } /// @@ -150,16 +130,10 @@ namespace Umbraco.Core /// /// Key [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheItem(string key) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheItem(key); - } - else - { - _httpCache.ClearCacheItem(key); - } + RuntimeCache.ClearCacheItem(key); } @@ -171,30 +145,17 @@ namespace Umbraco.Core [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] public void ClearCacheObjectTypes(string typeName) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheObjectTypes(typeName); - } - else - { - _httpCache.ClearCacheObjectTypes(typeName); - } + RuntimeCache.ClearCacheObjectTypes(typeName); } /// /// Clears all objects in the System.Web.Cache with the System.Type specified /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheObjectTypes() { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheObjectTypes(); - } - else - { - _httpCache.ClearCacheObjectTypes(); - } + RuntimeCache.ClearCacheObjectTypes(); } /// @@ -202,16 +163,10 @@ namespace Umbraco.Core /// /// The start of the key [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheByKeySearch(string keyStartsWith) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheByKeySearch(keyStartsWith); - } - else - { - _httpCache.ClearCacheByKeySearch(keyStartsWith); - } + RuntimeCache.ClearCacheByKeySearch(keyStartsWith); } /// @@ -219,29 +174,17 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void ClearCacheByKeyExpression(string regexString) { - if (_enableCache == false) - { - _nullHttpCache.ClearCacheByKeyExpression(regexString); - } - else - { - _httpCache.ClearCacheByKeyExpression(regexString); - } + RuntimeCache.ClearCacheByKeyExpression(regexString); } [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItemsByKeySearch(keyStartsWith); - } - else - { - return _httpCache.GetCacheItemsByKeySearch(keyStartsWith); - } + return RuntimeCache.GetCacheItemsByKeySearch(keyStartsWith); } /// @@ -251,16 +194,10 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey); - } - else - { - return _httpCache.GetCacheItem(cacheKey); - } + return RuntimeCache.GetCacheItem(cacheKey); } /// @@ -271,16 +208,11 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, Func getCacheItem) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem); + } /// @@ -292,17 +224,12 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem, timeout); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout); + } /// @@ -315,18 +242,13 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, CacheItemRemovedCallback refreshAction, TimeSpan timeout, Func getCacheItem) { - if (!_enableCache) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); + } /// @@ -340,18 +262,13 @@ namespace Umbraco.Core /// /// [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public TT GetCacheItem(string cacheKey, CacheItemPriority priority, CacheItemRemovedCallback refreshAction, TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) - { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); - } - else - { - return _httpCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); - } + return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); + } /// @@ -373,20 +290,13 @@ namespace Umbraco.Core TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction, null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); - return result == null ? default(TT) : result.TryConvertTo().Result; - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); + return result == null ? default(TT) : result.TryConvertTo().Result; } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } /// @@ -404,20 +314,13 @@ namespace Umbraco.Core CacheDependency cacheDependency, Func getCacheItem) { - if (!_enableCache) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - return _nullHttpCache.GetCacheItem(cacheKey, getCacheItem, null, false, priority, null, null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), null, false, priority, null, cacheDependency); - return result == null ? default(TT) : result.TryConvertTo().Result; - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), null, false, priority, null, cacheDependency); + return result == null ? default(TT) : result.TryConvertTo().Result; } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } /// @@ -427,18 +330,14 @@ namespace Umbraco.Core /// /// /// + [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void InsertCacheItem(string cacheKey, CacheItemPriority priority, Func getCacheItem) { - if (_enableCache == false) - { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); - } - else - { - _httpCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); - } + RuntimeCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); + } /// @@ -449,19 +348,14 @@ namespace Umbraco.Core /// /// This will set an absolute expiration from now until the timeout /// + [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] + [EditorBrowsable(EditorBrowsableState.Never)] public void InsertCacheItem(string cacheKey, CacheItemPriority priority, TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) - { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); - } - else - { - _httpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); - } + RuntimeCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); } /// @@ -480,19 +374,12 @@ namespace Umbraco.Core TimeSpan timeout, Func getCacheItem) { - if (_enableCache == false) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority, dependentFiles:null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, null, cacheDependency); - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, null, cacheDependency); } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } /// @@ -513,22 +400,29 @@ namespace Umbraco.Core TimeSpan? timeout, Func getCacheItem) { - if (_enableCache == false) + var cache = GetHttpRuntimeCacheProvider(RuntimeCache); + if (cache != null) { - _nullHttpCache.InsertCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction, null); - } - else - { - var cache = _httpCache as HttpRuntimeCacheProvider; - if (cache != null) - { - cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); + cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); } + throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); } #endregion - } + private HttpRuntimeCacheProvider GetHttpRuntimeCacheProvider(IRuntimeCacheProvider runtimeCache) + { + HttpRuntimeCacheProvider cache; + var wrapper = RuntimeCache as IRuntimeCacheProviderWrapper; + if (wrapper != null) + { + cache = wrapper.InnerProvider as HttpRuntimeCacheProvider; + } + else + { + cache = RuntimeCache as HttpRuntimeCacheProvider; + } + return cache; + } + } } diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs new file mode 100644 index 0000000000..5067562aa7 --- /dev/null +++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Collections +{ + /// + /// A List that can be deep cloned with deep cloned elements and can reset the collection's items dirty flags + /// + /// + internal class DeepCloneableList : List, IDeepCloneable, IRememberBeingDirty + { + private readonly ListCloneBehavior _listCloneBehavior; + + public DeepCloneableList(ListCloneBehavior listCloneBehavior) + { + _listCloneBehavior = listCloneBehavior; + } + + public DeepCloneableList(IEnumerable collection, ListCloneBehavior listCloneBehavior) : base(collection) + { + _listCloneBehavior = listCloneBehavior; + } + + /// + /// Default behavior is CloneOnce + /// + /// + public DeepCloneableList(IEnumerable collection) + : this(collection, ListCloneBehavior.CloneOnce) + { + } + + /// + /// Creates a new list and adds each element as a deep cloned element if it is of type IDeepCloneable + /// + /// + public object DeepClone() + { + switch (_listCloneBehavior) + { + case ListCloneBehavior.CloneOnce: + //we are cloning once, so create a new list in none mode + // and deep clone all items into it + var newList = new DeepCloneableList(ListCloneBehavior.None); + foreach (var item in this) + { + var dc = item as IDeepCloneable; + if (dc != null) + { + newList.Add((T)dc.DeepClone()); + } + else + { + newList.Add(item); + } + } + return newList; + case ListCloneBehavior.None: + //we are in none mode, so just return a new list with the same items + return new DeepCloneableList(this, ListCloneBehavior.None); + case ListCloneBehavior.Always: + //always clone to new list + var newList2 = new DeepCloneableList(ListCloneBehavior.Always); + foreach (var item in this) + { + var dc = item as IDeepCloneable; + if (dc != null) + { + newList2.Add((T)dc.DeepClone()); + } + else + { + newList2.Add(item); + } + } + return newList2; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public bool IsDirty() + { + return this.OfType().Any(x => x.IsDirty()); + } + + public bool WasDirty() + { + return this.OfType().Any(x => x.WasDirty()); + } + + /// + /// Always returns false, the list has no properties we need to report + /// + /// + /// + public bool IsPropertyDirty(string propName) + { + return false; + } + + /// + /// Always returns false, the list has no properties we need to report + /// + /// + /// + public bool WasPropertyDirty(string propertyName) + { + return false; + } + + public void ResetDirtyProperties() + { + foreach (var dc in this.OfType()) + { + dc.ResetDirtyProperties(); + } + } + + public void ForgetPreviouslyDirtyProperties() + { + foreach (var dc in this.OfType()) + { + dc.ForgetPreviouslyDirtyProperties(); + } + } + + public void ResetDirtyProperties(bool rememberPreviouslyChangedProperties) + { + foreach (var dc in this.OfType()) + { + dc.ResetDirtyProperties(rememberPreviouslyChangedProperties); + } + } + } +} diff --git a/src/Umbraco.Core/Collections/ListCloneBehavior.cs b/src/Umbraco.Core/Collections/ListCloneBehavior.cs new file mode 100644 index 0000000000..4fe935f7ff --- /dev/null +++ b/src/Umbraco.Core/Collections/ListCloneBehavior.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Collections +{ + internal enum ListCloneBehavior + { + /// + /// When set, DeepClone will clone the items one time and the result list behavior will be None + /// + CloneOnce, + + /// + /// When set, DeepClone will not clone any items + /// + None, + + /// + /// When set, DeepClone will always clone all items + /// + Always + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 2c3855727b..525bff2999 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Configuration //make this volatile so that we can ensure thread safety with a double check lock private static volatile string _reservedUrlsCache; private static string _reservedPathsCache; - private static StartsWithContainer _reservedList = new StartsWithContainer(); + private static HashSet _reservedList = new HashSet(); private static string _reservedPaths; private static string _reservedUrls; //ensure the built on (non-changeable) reserved paths are there at all times @@ -767,38 +767,31 @@ namespace Umbraco.Core.Configuration // store references to strings to determine changes _reservedPathsCache = GlobalSettings.ReservedPaths; _reservedUrlsCache = GlobalSettings.ReservedUrls; - - string _root = SystemDirectories.Root.Trim().ToLower(); - + // add URLs and paths to a new list - StartsWithContainer _newReservedList = new StartsWithContainer(); - foreach (string reservedUrl in _reservedUrlsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)) + var newReservedList = new HashSet(); + foreach (var reservedUrlTrimmed in _reservedUrlsCache + .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim().ToLowerInvariant()) + .Where(x => x.IsNullOrWhiteSpace() == false) + .Select(reservedUrl => IOHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/")) + .Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false)) { - if (string.IsNullOrWhiteSpace(reservedUrl)) - continue; - - - //resolves the url to support tilde chars - string reservedUrlTrimmed = IOHelper.ResolveUrl(reservedUrl.Trim()).Trim().ToLower(); - if (reservedUrlTrimmed.Length > 0) - _newReservedList.Add(reservedUrlTrimmed); + newReservedList.Add(reservedUrlTrimmed); } - foreach (string reservedPath in _reservedPathsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var reservedPathTrimmed in _reservedPathsCache + .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim().ToLowerInvariant()) + .Where(x => x.IsNullOrWhiteSpace() == false) + .Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/")) + .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false)) { - bool trimEnd = !reservedPath.EndsWith("/"); - if (string.IsNullOrWhiteSpace(reservedPath)) - continue; - - //resolves the url to support tilde chars - string reservedPathTrimmed = IOHelper.ResolveUrl(reservedPath.Trim()).Trim().ToLower(); - - if (reservedPathTrimmed.Length > 0) - _newReservedList.Add(reservedPathTrimmed + (reservedPathTrimmed.EndsWith("/") ? "" : "/")); + newReservedList.Add(reservedPathTrimmed); } // use the new list from now on - _reservedList = _newReservedList; + _reservedList = newReservedList; } } } @@ -806,107 +799,17 @@ namespace Umbraco.Core.Configuration //The url should be cleaned up before checking: // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/' // * We shouldn't be comparing the query at all - var pathPart = url.Split('?')[0]; - if (!pathPart.Contains(".") && !pathPart.EndsWith("/")) + var pathPart = url.Split(new[] {'?'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant(); + if (pathPart.Contains(".") == false) { - pathPart += "/"; + pathPart = pathPart.EnsureEndsWith('/'); } // return true if url starts with an element of the reserved list - return _reservedList.StartsWith(pathPart.ToLowerInvariant()); + return _reservedList.Any(x => pathPart.InvariantStartsWith(x)); } - /// - /// Structure that checks in logarithmic time - /// if a given string starts with one of the added keys. - /// - private class StartsWithContainer - { - /// Internal sorted list of keys. - public SortedList _list - = new SortedList(StartsWithComparator.Instance); - - /// - /// Adds the specified new key. - /// - /// The new key. - public void Add(string newKey) - { - // if the list already contains an element that begins with newKey, return - if (String.IsNullOrEmpty(newKey) || StartsWith(newKey)) - return; - - // create a new collection, so the old one can still be accessed - SortedList newList - = new SortedList(_list.Count + 1, StartsWithComparator.Instance); - - // add only keys that don't already start with newKey, others are unnecessary - foreach (string key in _list.Keys) - if (!key.StartsWith(newKey)) - newList.Add(key, null); - // add the new key - newList.Add(newKey, null); - - // update the list (thread safe, _list was never in incomplete state) - _list = newList; - } - - /// - /// Checks if the given string starts with any of the added keys. - /// - /// The target. - /// true if a key is found that matches the start of target - /// - /// Runs in O(s*log(n)), with n the number of keys and s the length of target. - /// - public bool StartsWith(string target) - { - return _list.ContainsKey(target); - } - - /// Comparator that tests if a string starts with another. - /// Not a real comparator, since it is not reflexive. (x==y does not imply y==x) - private sealed class StartsWithComparator : IComparer - { - /// Default string comparer. - private readonly static Comparer _stringComparer = Comparer.Default; - - /// Gets an instance of the StartsWithComparator. - public static readonly StartsWithComparator Instance = new StartsWithComparator(); - - /// - /// Tests if whole begins with all characters of part. - /// - /// The part. - /// The whole. - /// - /// Returns 0 if whole starts with part, otherwise performs standard string comparison. - /// - public int Compare(string part, string whole) - { - // let the default string comparer deal with null or when part is not smaller then whole - if (part == null || whole == null || part.Length >= whole.Length) - return _stringComparer.Compare(part, whole); - - ////ensure both have a / on the end - //part = part.EndsWith("/") ? part : part + "/"; - //whole = whole.EndsWith("/") ? whole : whole + "/"; - //if (part.Length >= whole.Length) - // return _stringComparer.Compare(part, whole); - - // loop through all characters that part and whole have in common - int pos = 0; - bool match; - do - { - match = (part[pos] == whole[pos]); - } while (match && ++pos < part.Length); - - // return result of last comparison - return match ? 0 : (part[pos] < whole[pos] ? -1 : 1); - } - } - } + } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index ced63f04bb..1b1404a897 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -280,6 +280,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } } + [ConfigurationProperty("EnableInheritedDocumentTypes")] + internal InnerTextConfigurationElement EnableInheritedDocumentTypes + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], + //set the default + true); + } + } + string IContentSection.NotificationEmailAddress { get { return Notifications.NotificationEmailAddress; } @@ -414,5 +426,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return DefaultDocumentTypeProperty; } } + + bool IContentSection.EnableInheritedDocumentTypes + { + get { return EnableInheritedDocumentTypes; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 93e3260b44..ebdd9ae637 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -59,5 +59,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool GlobalPreviewStorageEnabled { get; } string DefaultDocumentTypeProperty { get; } + + bool EnableInheritedDocumentTypes { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs index c3a1df301d..c44c0cf0df 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ISecuritySection.cs @@ -6,6 +6,8 @@ bool HideDisabledUsersInBackoffice { get; } + bool AllowPasswordReset { get; } + string AuthCookieName { get; } string AuthCookieDomain { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index 34642c8c90..f280b3e20c 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -28,6 +28,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } } + [ConfigurationProperty("allowPasswordReset")] + internal InnerTextConfigurationElement AllowPasswordReset + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement)this["allowPasswordReset"], + //set the default + true); + } + } + [ConfigurationProperty("authCookieName")] internal InnerTextConfigurationElement AuthCookieName { @@ -62,6 +74,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return HideDisabledUsersInBackoffice; } } + bool ISecuritySection.AllowPasswordReset + { + get { return AllowPasswordReset; } + } + string ISecuritySection.AuthCookieName { get { return AuthCookieName; } diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index e61d091d84..762d3da59c 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.3.0"); + private static readonly Version Version = new Version("7.5.0"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index f03108f4ac..12f7076fc4 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -43,7 +43,7 @@ public const string Users = "users"; /// - /// Application alias for the users section. + /// Application alias for the forms section. /// public const string Forms = "forms"; } @@ -59,7 +59,7 @@ public const string Content = "content"; /// - /// alias for the media tree. + /// alias for the member tree. /// public const string Members = "member"; @@ -71,7 +71,7 @@ /// /// alias for the datatype tree. /// - public const string DataTypes = "datatype"; + public const string DataTypes = "dataTypes"; /// /// alias for the dictionary tree. @@ -80,6 +80,22 @@ public const string Stylesheets = "stylesheets"; + /// + /// alias for the document type tree. + /// + public const string DocumentTypes = "documentTypes"; + + /// + /// alias for the media type tree. + /// + public const string MediaTypes = "mediaTypes"; + + + /// + /// alias for the member type tree. + /// + public const string MemberTypes = "memberTypes"; + /// /// alias for the template tree. /// diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 3456ff1cfa..7e2bb88964 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -206,7 +206,7 @@ namespace Umbraco.Core /// internal const string StandardPropertiesGroupName = "Membership"; - internal static Dictionary GetStandardPropertyTypeStubs() + public static Dictionary GetStandardPropertyTypeStubs() { return new Dictionary { diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs new file mode 100644 index 0000000000..a0928919f0 --- /dev/null +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class Icons + { + + + /// + /// System contenttype icon + /// + public const string ContentType = "icon-arrangement"; + + /// + /// System datatype icon + /// + public const string DataType = "icon-autofill"; + + /// + /// System property editor icon + /// + public const string PropertyEditor = "icon-autofill"; + + /// + /// System macro icon + /// + public const string Macro = "icon-settings-alt"; + + /// + /// System member icon + /// + public const string Member = "icon-user"; + + /// + /// System member icon + /// + public const string MemberType = "icon-users"; + + + /// + /// System member icon + /// + public const string Template = "icon-layout"; + + } + } +} diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index eecf0fb1fc..7ec45db7be 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -9,6 +9,36 @@ namespace Umbraco.Core /// public static class ObjectTypes { + /// + /// Guid for a data type container + /// + public const string DataTypeContainer = "521231E3-8B37-469C-9F9D-51AFC91FEB7B"; + + /// + /// Guid for a data type container + /// + public static readonly Guid DataTypeContainerGuid = new Guid(DataTypeContainer); + + /// + /// Guid for a doc type container + /// + public const string DocumentTypeContainer = "2F7A2769-6B0B-4468-90DD-AF42D64F7F16"; + + /// + /// Guid for a doc type container + /// + public static readonly Guid DocumentTypeContainerGuid = new Guid(DocumentTypeContainer); + + /// + /// Guid for a doc type container + /// + public const string MediaTypeContainer = "42AEF799-B288-4744-9B10-BE144B73CDC4"; + + /// + /// Guid for a doc type container + /// + public static readonly Guid MediaTypeContainerGuid = new Guid(MediaTypeContainer); + /// /// Guid for a Content Item object. /// @@ -29,6 +59,11 @@ namespace Umbraco.Core /// public const string DataType = "30A2A501-1978-4DDB-A57B-F7EFED43BA3C"; + /// + /// Guid for a DataType object. + /// + public static readonly Guid DataTypeGuid = new Guid(DataType); + /// /// Guid for a Document object. /// @@ -39,6 +74,11 @@ namespace Umbraco.Core /// public const string DocumentType = "A2CB7800-F571-4787-9638-BC48539A0EFB"; + /// + /// Guid for a Document Type object. + /// + public static readonly Guid DocumentTypeGuid = new Guid(DocumentType); + /// /// Guid for a Media object. /// @@ -54,6 +94,11 @@ namespace Umbraco.Core /// public const string MediaType = "4EA4382B-2F5A-4C2B-9587-AE9B3CF3602E"; + /// + /// Guid for a Media Type object. + /// + public static readonly Guid MediaTypeGuid = new Guid(MediaType); + /// /// Guid for a Member object. /// @@ -69,6 +114,11 @@ namespace Umbraco.Core /// public const string MemberType = "9B5416FB-E72F-45A9-A07B-5A9A2709CE43"; + /// + /// Guid for a Member Type object. + /// + public static readonly Guid MemberTypeGuid = new Guid(MemberType); + /// /// Guid for a Stylesheet object. /// @@ -92,6 +142,11 @@ namespace Umbraco.Core /// Guid for a Lock object. /// public const string LockObject = "87A9F1FF-B1E4-4A25-BABB-465A4A47EC41"; + + /// + /// Guid for a Lock object. + /// + public static readonly Guid LockObjectGuid = new Guid(LockObject); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index a719e845b1..80f118b58e 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core { /// /// Used to prefix generic properties that are internal content properties - /// + /// public const string InternalGenericPropertiesPrefix = "_umb_"; /// @@ -74,7 +74,7 @@ namespace Umbraco.Core /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string DictionaryPicker = "17B70066-F764-407D-AB05-3717F1E1C513"; - + /// /// Guid for the Dropdown list datatype. /// @@ -158,6 +158,11 @@ namespace Umbraco.Core /// public const string IntegerAlias = "Umbraco.Integer"; + /// + /// Alias for the Decimal datatype. + /// + public const string DecimalAlias = "Umbraco.Decimal"; + /// /// Alias for the listview datatype. /// @@ -310,13 +315,13 @@ namespace Umbraco.Core public const string TextboxAlias = "Umbraco.Textbox"; /// - /// Guid for the Textbox multiple datatype. + /// Guid for the Textarea datatype. /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string TextboxMultiple = "67DB8357-EF57-493E-91AC-936D305E0F2A"; /// - /// Alias for the Textbox multiple datatype. + /// Alias for the Textarea datatype. /// public const string TextboxMultipleAlias = "Umbraco.TextboxMultiple"; @@ -347,7 +352,7 @@ namespace Umbraco.Core /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string UltimatePicker = "CDBF0B5D-5CB2-445F-BC12-FCAAEC07CF2C"; - + /// /// Guid for the UltraSimpleEditor datatype. /// @@ -364,7 +369,7 @@ namespace Umbraco.Core /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string UmbracoUserControlWrapper = "D15E1281-E456-4B24-AA86-1DDA3E4299D5"; - + /// /// Guid for the Upload field datatype. /// @@ -414,6 +419,15 @@ namespace Umbraco.Core /// Alias for the email address property editor /// public const string EmailAddressAlias = "Umbraco.EmailAddress"; + + public static class PreValueKeys + { + /// + /// Pre-value name used to indicate a field that can be used to override the database field to which data for the associated + /// property is saved + /// + public const string DataValueType = "umbracoDataValueType"; + } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs new file mode 100644 index 0000000000..5dabe90029 --- /dev/null +++ b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs @@ -0,0 +1,31 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + /// + /// Defines the identifiers for property-type groups conventions that are used within the Umbraco core. + /// + public static class PropertyTypeGroups + { + /// + /// Guid for a Image PropertyTypeGroup object. + /// + public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; + + /// + /// Guid for a File PropertyTypeGroup object. + /// + public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; + + /// + /// Guid for a Image PropertyTypeGroup object. + /// + public const string Contents = "79995FA2-63EE-453C-A29B-2E66F324CDBE"; + + /// + /// Guid for a Image PropertyTypeGroup object. + /// + public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 13dee96b97..f596820506 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Core +using System; +using System.ComponentModel; + +namespace Umbraco.Core { public static partial class Constants { @@ -7,6 +10,12 @@ /// public static class Web { + public const string UmbracoContextDataToken = "umbraco-context"; + public const string UmbracoDataToken = "umbraco"; + public const string PublishedDocumentRequestDataToken = "umbraco-doc-request"; + public const string CustomRouteDataToken = "umbraco-custom-route"; + public const string UmbracoRouteDefinitionDataToken = "umbraco-route-def"; + /// /// The preview cookie name /// @@ -15,6 +24,8 @@ /// /// The auth cookie name /// + [Obsolete("DO NOT USE THIS, USE ISecuritySection.AuthCookieName, this will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string AuthCookieName = "UMB_UCONTEXT"; } diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index b4e3d06273..4da740f458 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -136,7 +136,10 @@ namespace Umbraco.Core { try { - x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationInitialized", x.GetType()))) + { + x.OnApplicationInitialized(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -188,10 +191,16 @@ namespace Umbraco.Core protected virtual CacheHelper CreateApplicationCache() { var cacheHelper = new CacheHelper( - new ObjectCacheRuntimeCacheProvider(), + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()), new StaticCacheProvider(), //we have no request based cache when not running in web-based context - new NullCacheProvider()); + new NullCacheProvider(), + new IsolatedRuntimeCache(type => + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); return cacheHelper; } @@ -293,7 +302,10 @@ namespace Umbraco.Core { try { - x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarting", x.GetType()))) + { + x.OnApplicationStarting(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -344,7 +356,10 @@ namespace Umbraco.Core { try { - x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + using (ProfilingLogger.DebugDuration(string.Format("Executing {0} in ApplicationStarted", x.GetType()))) + { + x.OnApplicationStarted(UmbracoApplication, ApplicationContext); + } } catch (Exception ex) { @@ -431,6 +446,7 @@ namespace Umbraco.Core new Lazy(() => typeof (DelimitedManifestValueValidator)), new Lazy(() => typeof (EmailValidator)), new Lazy(() => typeof (IntegerValidator)), + new Lazy(() => typeof (DecimalValidator)), }); //by default we'll use the db server registrar unless the developer has the legacy diff --git a/src/Umbraco.Core/DisposableTimer.cs b/src/Umbraco.Core/DisposableTimer.cs index 816256360a..c7e8874449 100644 --- a/src/Umbraco.Core/DisposableTimer.cs +++ b/src/Umbraco.Core/DisposableTimer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Web; @@ -12,6 +13,12 @@ namespace Umbraco.Core /// public class DisposableTimer : DisposableObject { + private readonly ILogger _logger; + private readonly LogType? _logType; + private readonly IProfiler _profiler; + private readonly Type _loggerType; + private readonly string _endMessage; + private readonly IDisposable _profilerStep; private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Action _callback; @@ -25,25 +32,12 @@ namespace Umbraco.Core if (logger == null) throw new ArgumentNullException("logger"); if (loggerType == null) throw new ArgumentNullException("loggerType"); - _callback = x => - { - if (profiler != null) - { - profiler.DisposeIfDisposable(); - } - switch (logType) - { - case LogType.Debug: - logger.Debug(loggerType, () => endMessage + " (took " + x + "ms)"); - break; - case LogType.Info: - logger.Info(loggerType, () => endMessage + " (took " + x + "ms)"); - break; - default: - throw new ArgumentOutOfRangeException("logType"); - } - - }; + _logger = logger; + _logType = logType; + _profiler = profiler; + _loggerType = loggerType; + _endMessage = endMessage; + switch (logType) { case LogType.Debug: @@ -58,7 +52,7 @@ namespace Umbraco.Core if (profiler != null) { - profiler.Step(loggerType, startMessage); + _profilerStep = profiler.Step(loggerType, startMessage); } } @@ -223,7 +217,36 @@ namespace Umbraco.Core /// protected override void DisposeResources() { - _callback.Invoke(Stopwatch.ElapsedMilliseconds); + if (_profiler != null) + { + _profiler.DisposeIfDisposable(); + } + + if (_profilerStep != null) + { + _profilerStep.Dispose(); + } + + if (_logType.HasValue && _endMessage.IsNullOrWhiteSpace() == false && _loggerType != null && _logger != null) + { + switch (_logType) + { + case LogType.Debug: + _logger.Debug(_loggerType, () => _endMessage + " (took " + Stopwatch.ElapsedMilliseconds + "ms)"); + break; + case LogType.Info: + _logger.Info(_loggerType, () => _endMessage + " (took " + Stopwatch.ElapsedMilliseconds + "ms)"); + break; + default: + throw new ArgumentOutOfRangeException("logType"); + } + } + + if (_callback != null) + { + _callback.Invoke(Stopwatch.ElapsedMilliseconds); + } + } } diff --git a/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs b/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs new file mode 100644 index 0000000000..43d7adc647 --- /dev/null +++ b/src/Umbraco.Core/Dynamics/CaseInsensitiveDynamicObject.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Reflection; + +namespace Umbraco.Core.Dynamics +{ + /// + /// This will check enable dynamic access to properties and methods in a case insensitive manner + /// + /// + /// + /// This works by using reflection on the type - the reflection lookup is lazy so it will not execute unless a dynamic method needs to be accessed + /// + public abstract class CaseInsensitiveDynamicObject : DynamicObject + where T: class + { + /// + /// Used for dynamic access for case insensitive property access + /// ` + private static readonly Lazy>> CaseInsensitivePropertyAccess = new Lazy>>(() => + { + var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .DistinctBy(x => x.Name); + return props.Select(propInfo => + { + var name = propInfo.Name.ToLowerInvariant(); + Func getVal = propInfo.GetValue; + return new KeyValuePair>(name, getVal); + + }).ToDictionary(x => x.Key, x => x.Value); + }); + + /// + /// Used for dynamic access for case insensitive property access + /// + private static readonly Lazy>>> CaseInsensitiveMethodAccess + = new Lazy>>>(() => + { + var props = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.IsSpecialName == false && x.IsVirtual == false) + .DistinctBy(x => x.Name); + return props.Select(methodInfo => + { + var name = methodInfo.Name.ToLowerInvariant(); + Func getVal = methodInfo.Invoke; + var val = new Tuple>(methodInfo.GetParameters(), getVal); + return new KeyValuePair>>(name, val); + + }).ToDictionary(x => x.Key, x => x.Value); + }); + + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) + { + var name = binder.Name.ToLowerInvariant(); + if (CaseInsensitiveMethodAccess.Value.ContainsKey(name) == false) + return base.TryInvokeMember(binder, args, out result); + + var val = CaseInsensitiveMethodAccess.Value[name]; + var parameters = val.Item1; + var callback = val.Item2; + var fullArgs = new List(args); + if (args.Length <= parameters.Length) + { + //need to fill them up if they're optional + for (var i = args.Length; i < parameters.Length; i++) + { + if (parameters[i].IsOptional) + { + fullArgs.Add(parameters[i].DefaultValue); + } + } + if (fullArgs.Count == parameters.Length) + { + result = callback((T)(object)this, fullArgs.ToArray()); + return true; + } + } + return base.TryInvokeMember(binder, args, out result); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var name = binder.Name.ToLowerInvariant(); + if (CaseInsensitivePropertyAccess.Value.ContainsKey(name) == false) + return base.TryGetMember(binder, out result); + + result = CaseInsensitivePropertyAccess.Value[name]((T)(object)this); + return true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs b/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs index f4ace38465..6af13d6887 100644 --- a/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs +++ b/src/Umbraco.Core/Dynamics/DynamicXmlConverter.cs @@ -64,7 +64,7 @@ namespace Umbraco.Core.Dynamics /// public class DynamicXmlConverter : TypeConverter { - public override bool CanConvertTo(ITypeDescriptorContext context, Type sourceType) + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { var convertableTypes = new[] { @@ -78,8 +78,8 @@ namespace Umbraco.Core.Dynamics typeof(RawXmlDocument) }; - return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, sourceType)) - || base.CanConvertFrom(context, sourceType); + return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + || base.CanConvertFrom(context, destinationType); } public override object ConvertTo( diff --git a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs index 4430d1d74e..9772aaf5bd 100644 --- a/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs +++ b/src/Umbraco.Core/Dynamics/ExtensionMethodFinder.cs @@ -5,8 +5,10 @@ using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Linq.Expressions; +using System.Text; using System.Web.Services.Description; using Umbraco.Core.Cache; +using Umbraco.Core.Logging; namespace Umbraco.Core.Dynamics { @@ -20,6 +22,20 @@ namespace Umbraco.Core.Dynamics /// private static readonly ConcurrentDictionary, MethodInfo[]> MethodCache = new ConcurrentDictionary, MethodInfo[]>(); + private static IEnumerable GetTypes(Assembly a) + { + try + { + return TypeFinder.GetTypesWithFormattedException(a); + } + catch (ReflectionTypeLoadException ex) + { + // is this going to flood the log? + LogHelper.Error(typeof (ExtensionMethodFinder), "Failed to get types.", ex); + return Enumerable.Empty(); + } + } + /// /// Returns the enumerable of all extension method info's in the app domain = USE SPARINGLY!!! /// @@ -36,7 +52,7 @@ namespace Umbraco.Core.Dynamics // assemblies that contain extension methods .Where(a => a.IsDefined(typeof (ExtensionAttribute), false)) // types that contain extension methods - .SelectMany(a => a.GetTypes() + .SelectMany(a => GetTypes(a) .Where(t => t.IsDefined(typeof (ExtensionAttribute), false) && t.IsSealed && t.IsGenericType == false && t.IsNested == false)) // actual extension methods .SelectMany(t => t.GetMethods(BindingFlags.Static | BindingFlags.Public) @@ -45,9 +61,9 @@ namespace Umbraco.Core.Dynamics .Concat(typeof (Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)) //If we don't do this then we'll be scanning all assemblies each time! .ToArray(), - + //only cache for 5 minutes - timeout: TimeSpan.FromMinutes(5), + timeout: TimeSpan.FromMinutes(5), //each time this is accessed it will be for 5 minutes longer isSliding:true); @@ -57,7 +73,7 @@ namespace Umbraco.Core.Dynamics /// Returns all extension methods found matching the definition /// /// - /// The runtime cache is used to temporarily cache all extension methods found in the app domain so that + /// The runtime cache is used to temporarily cache all extension methods found in the app domain so that /// while we search for individual extension methods, the process will be reasonably 'quick'. We then statically /// cache the MethodInfo's that we are looking for and then the runtime cache will expire and give back all that memory. /// @@ -78,7 +94,7 @@ namespace Umbraco.Core.Dynamics { var candidates = GetAllExtensionMethodsInAppDomain(runtimeCache); - // filter by name + // filter by name var filtr1 = candidates.Where(m => m.Name == name); // filter by args count @@ -102,7 +118,7 @@ namespace Umbraco.Core.Dynamics return filtr3.ToArray(); }); - + } private static MethodInfo DetermineMethodFromParams(IEnumerable methods, Type genericType, IEnumerable args) @@ -123,12 +139,12 @@ namespace Umbraco.Core.Dynamics types = method.GetParameters().Select(pi => pi.ParameterType).Skip(1) }); - //This type comparer will check + //This type comparer will check var typeComparer = new DelegateEqualityComparer( - //Checks if the argument type passed in can be assigned from the parameter type in the method. For + //Checks if the argument type passed in can be assigned from the parameter type in the method. For // example, if the argument type is HtmlHelper but the method parameter type is HtmlHelper then // it will match because the argument is assignable to that parameter type and will be able to execute - TypeHelper.IsTypeAssignableFrom, + TypeHelper.IsTypeAssignableFrom, //This will not ever execute but if it does we need to get the hash code of the string because the hash // code of a type is random type => type.FullName.GetHashCode()); @@ -159,7 +175,7 @@ namespace Umbraco.Core.Dynamics .ToArray(); var methods = GetAllExtensionMethods(runtimeCache, thisType, name, args.Length).ToArray(); - + return DetermineMethodFromParams(methods, genericType, args); } } diff --git a/src/Umbraco.Core/Events/CancellableEventArgs.cs b/src/Umbraco.Core/Events/CancellableEventArgs.cs index 506ba1c22e..72cef19f7a 100644 --- a/src/Umbraco.Core/Events/CancellableEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableEventArgs.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Security.Permissions; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Events { @@ -11,11 +14,19 @@ namespace Umbraco.Core.Events { private bool _cancel; + public CancellableEventArgs(bool canCancel, EventMessages messages, IDictionary additionalData) + { + CanCancel = canCancel; + Messages = messages; + AdditionalData = new ReadOnlyDictionary(additionalData); + } + public CancellableEventArgs(bool canCancel, EventMessages eventMessages) { if (eventMessages == null) throw new ArgumentNullException("eventMessages"); CanCancel = canCancel; Messages = eventMessages; + AdditionalData = new ReadOnlyDictionary(new Dictionary()); } public CancellableEventArgs(bool canCancel) @@ -23,6 +34,7 @@ namespace Umbraco.Core.Events CanCancel = canCancel; //create a standalone messages Messages = new EventMessages(); + AdditionalData = new ReadOnlyDictionary(new Dictionary()); } public CancellableEventArgs(EventMessages eventMessages) @@ -78,5 +90,14 @@ namespace Umbraco.Core.Events /// Returns the EventMessages object which is used to add messages to the message collection for this event /// public EventMessages Messages { get; private set; } + + /// + /// In some cases raised evens might need to contain additional arbitrary readonly data which can be read by event subscribers + /// + /// + /// This allows for a bit of flexibility in our event raising - it's not pretty but we need to maintain backwards compatibility + /// so we cannot change the strongly typed nature for some events. + /// + public ReadOnlyDictionary AdditionalData { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs index 726d32d7b0..a05f09ece5 100644 --- a/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs +++ b/src/Umbraco.Core/Events/CancellableObjectEventArgs.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Security.Permissions; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -11,8 +12,13 @@ namespace Umbraco.Core.Events [HostProtection(SecurityAction.LinkDemand, SharedState = true)] public class CancellableObjectEventArgs : CancellableEventArgs { + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(canCancel, messages, additionalData) + { + EventObject = eventObject; + } - public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) + public CancellableObjectEventArgs(T eventObject, bool canCancel, EventMessages eventMessages) : base(canCancel, eventMessages) { EventObject = eventObject; diff --git a/src/Umbraco.Core/Events/EventExtensions.cs b/src/Umbraco.Core/Events/EventExtensions.cs index 8c645aead6..700a02457a 100644 --- a/src/Umbraco.Core/Events/EventExtensions.cs +++ b/src/Umbraco.Core/Events/EventExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace Umbraco.Core.Events diff --git a/src/Umbraco.Core/Events/MigrationEventArgs.cs b/src/Umbraco.Core/Events/MigrationEventArgs.cs index 7eed61484b..008e50d2ee 100644 --- a/src/Umbraco.Core/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Core/Events/MigrationEventArgs.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using Semver; +using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.Migrations; namespace Umbraco.Core.Events @@ -13,23 +14,49 @@ namespace Umbraco.Core.Events /// /// /// + /// /// /// - public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) - : base(eventObject, canCancel) - { - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - } + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : this(eventObject, null, configuredVersion, targetVersion, productName, canCancel) + { } - [Obsolete("Use constructor accepting UmbracoVersion instances instead")] + /// + /// Constructor accepting multiple migrations that are used in the migration runner + /// + /// + /// + /// + /// + [Obsolete("Use constructor accepting a product name instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) + : this(eventObject, null, configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName, canCancel) + { } + + [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion, bool canCancel) - : base(eventObject, canCancel) - { - ConfiguredSemVersion = new SemVersion(configuredVersion); - TargetSemVersion = new SemVersion(targetVersion); - } + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, canCancel) + { } + + /// + /// Constructor accepting multiple migrations that are used in the migration runner + /// + /// + /// + /// + /// + /// + /// + internal MigrationEventArgs(IList eventObject, MigrationContext migrationContext, SemVersion configuredVersion, SemVersion targetVersion, string productName, bool canCancel) + : base(eventObject, canCancel) + { + MigrationContext = migrationContext; + ConfiguredSemVersion = configuredVersion; + TargetSemVersion = targetVersion; + ProductName = productName; + } /// /// Constructor accepting multiple migrations that are used in the migration runner @@ -39,12 +66,15 @@ namespace Umbraco.Core.Events /// /// /// + [Obsolete("Use constructor accepting a product name instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] internal MigrationEventArgs(IList eventObject, MigrationContext migrationContext, SemVersion configuredVersion, SemVersion targetVersion, bool canCancel) : base(eventObject, canCancel) { MigrationContext = migrationContext; ConfiguredSemVersion = configuredVersion; TargetSemVersion = targetVersion; + ProductName = GlobalSettings.UmbracoMigrationName; } /// @@ -53,21 +83,28 @@ namespace Umbraco.Core.Events /// /// /// - public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion) - : base(eventObject) - { - ConfiguredSemVersion = configuredVersion; - TargetSemVersion = targetVersion; - } + /// + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion, string productName) + : this(eventObject, null, configuredVersion, targetVersion, productName, false) + { } - [Obsolete("Use constructor accepting UmbracoVersion instances instead")] + /// + /// Constructor accepting multiple migrations that are used in the migration runner + /// + /// + /// + /// + [Obsolete("Use constructor accepting a product name instead.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public MigrationEventArgs(IList eventObject, SemVersion configuredVersion, SemVersion targetVersion) + : this(eventObject, null, configuredVersion, targetVersion, GlobalSettings.UmbracoMigrationName, false) + { } + + [Obsolete("Use constructor accepting SemVersion instances and a product name instead.")] [EditorBrowsable(EditorBrowsableState.Never)] public MigrationEventArgs(IList eventObject, Version configuredVersion, Version targetVersion) - : base(eventObject) - { - ConfiguredSemVersion = new SemVersion(configuredVersion); - TargetSemVersion = new SemVersion(targetVersion); - } + : this(eventObject, null, new SemVersion(configuredVersion), new SemVersion(targetVersion), GlobalSettings.UmbracoMigrationName, false) + { } /// /// Returns all migrations that were used in the migration runner @@ -95,6 +132,8 @@ namespace Umbraco.Core.Events public SemVersion TargetSemVersion { get; private set; } + public string ProductName { get; private set; } + internal MigrationContext MigrationContext { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/SaveEventArgs.cs b/src/Umbraco.Core/Events/SaveEventArgs.cs index b84e28285e..dafd326e1c 100644 --- a/src/Umbraco.Core/Events/SaveEventArgs.cs +++ b/src/Umbraco.Core/Events/SaveEventArgs.cs @@ -4,6 +4,18 @@ namespace Umbraco.Core.Events { public class SaveEventArgs : CancellableObjectEventArgs> { + /// + /// Constructor accepting multiple entities that are used in the saving operation + /// + /// + /// + /// + /// + public SaveEventArgs(IEnumerable eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(eventObject, canCancel, messages, additionalData) + { + } + /// /// Constructor accepting multiple entities that are used in the saving operation /// @@ -25,12 +37,24 @@ namespace Umbraco.Core.Events { } - /// - /// Constructor accepting a single entity instance - /// - /// - /// - public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) + /// + /// Constructor accepting a single entity instance + /// + /// + /// + /// + /// + public SaveEventArgs(TEntity eventObject, bool canCancel, EventMessages messages, IDictionary additionalData) + : base(new List { eventObject }, canCancel, messages, additionalData) + { + } + + /// + /// Constructor accepting a single entity instance + /// + /// + /// + public SaveEventArgs(TEntity eventObject, EventMessages eventMessages) : base(new List { eventObject }, eventMessages) { } diff --git a/src/Umbraco.Core/Exceptions/DataOperationException.cs b/src/Umbraco.Core/Exceptions/DataOperationException.cs new file mode 100644 index 0000000000..9a66e6a5be --- /dev/null +++ b/src/Umbraco.Core/Exceptions/DataOperationException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Umbraco.Core.Exceptions +{ + internal class DataOperationException : Exception + { + public T Operation { get; private set; } + + public DataOperationException(T operation, string message) + :base(message) + { + Operation = operation; + } + + public DataOperationException(T operation) + : base("Data operation exception: " + operation) + { + Operation = operation; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs index fcf7a44677..bb9becf058 100644 --- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs +++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs @@ -3,22 +3,41 @@ namespace Umbraco.Core.Exceptions { public class InvalidCompositionException : Exception - { - public string ContentTypeAlias { get; set; } + { + public InvalidCompositionException(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliass) + { + ContentTypeAlias = contentTypeAlias; + AddedCompositionAlias = addedCompositionAlias; + PropertyTypeAliases = propertyTypeAliass; + } - public string AddedCompositionAlias { get; set; } + public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliass) + { + ContentTypeAlias = contentTypeAlias; + PropertyTypeAliases = propertyTypeAliass; + } - public string PropertyTypeAlias { get; set; } + public string ContentTypeAlias { get; private set; } + + public string AddedCompositionAlias { get; private set; } + + public string[] PropertyTypeAliases { get; private set; } public override string Message { get { - return string.Format( - "InvalidCompositionException - ContentType with alias '{0}' was added as a Compsition to ContentType with alias '{1}', " + - "but there was a conflict on the PropertyType alias '{2}'. " + + return AddedCompositionAlias.IsNullOrWhiteSpace() + ? string.Format( + "ContentType with alias '{0}' has an invalid composition " + + "and there was a conflict on the following PropertyTypes: '{1}'. " + "PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.", - AddedCompositionAlias, ContentTypeAlias, PropertyTypeAlias); + ContentTypeAlias, string.Join(", ", PropertyTypeAliases)) + : string.Format( + "ContentType with alias '{0}' was added as a Composition to ContentType with alias '{1}', " + + "but there was a conflict on the following PropertyTypes: '{2}'. " + + "PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.", + AddedCompositionAlias, ContentTypeAlias, string.Join(", ", PropertyTypeAliases)); } } } diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 433d01c206..98fdb01ff4 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -154,7 +154,7 @@ namespace Umbraco.Core.IO { get { - return IOHelper.ReturnPath("umbracoWebservicesPath", "~/umbraco/webservices"); + return IOHelper.ReturnPath("umbracoWebservicesPath", Umbraco.EnsureEndsWith("/") + "webservices"); } } diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index b8fe310f54..d708fcb804 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -199,8 +199,8 @@ namespace Umbraco.Core.IO using (var image = Image.FromStream(fs)) { var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL.jpg", Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal))) - : string.Format("{0}_{1}.jpg", Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); + ? string.Format("{0}_UMBRACOSYSTHUMBNAIL." + Extension, Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal))) + : string.Format("{0}_{1}." + Extension, Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); var thumbnail = maxWidthHeight == -1 ? ImageHelper.GenerateThumbnail(image, width, height, fileNameThumb, Extension, _fs) diff --git a/src/Umbraco.Core/IO/ViewHelper.cs b/src/Umbraco.Core/IO/ViewHelper.cs index 28dad49dc0..933a9d2956 100644 --- a/src/Umbraco.Core/IO/ViewHelper.cs +++ b/src/Umbraco.Core/IO/ViewHelper.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.IO { - internal class ViewHelper + public class ViewHelper { private readonly IFileSystem _viewFileSystem; @@ -66,17 +66,58 @@ namespace Umbraco.Core.IO return viewContent; } - internal static string GetDefaultFileContent(string layoutPageAlias = null) + public static string GetDefaultFileContent(string layoutPageAlias = null, string modelClassName = null, string modelNamespace = null, string modelNamespaceAlias = null) { - var design = @"@inherits Umbraco.Web.Mvc.UmbracoTemplatePage -@{ - Layout = null; -}"; + var content = new StringBuilder(); - if (layoutPageAlias.IsNullOrWhiteSpace() == false) - design = design.Replace("null", string.Format("\"{0}.cshtml\"", layoutPageAlias)); + if (string.IsNullOrWhiteSpace(modelNamespaceAlias)) + modelNamespaceAlias = "ContentModels"; - return design; + // either + // @inherits Umbraco.Web.Mvc.UmbracoTemplatePage + // @inherits Umbraco.Web.Mvc.UmbracoTemplatePage + // @inherits Umbraco.Web.Mvc.UmbracoTemplatePage + content.Append("@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"); + if (modelClassName.IsNullOrWhiteSpace() == false) + { + content.Append("<"); + if (modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append(modelNamespaceAlias); + content.Append("."); + } + content.Append(modelClassName); + content.Append(">"); + } + content.Append("\r\n"); + + // if required, add + // @using ContentModels = ModelNamespace; + if (modelClassName.IsNullOrWhiteSpace() == false && modelNamespace.IsNullOrWhiteSpace() == false) + { + content.Append("@using "); + content.Append(modelNamespaceAlias); + content.Append(" = "); + content.Append(modelNamespace); + content.Append(";\r\n"); + } + + // either + // Layout = null; + // Layout = "layoutPage.cshtml"; + content.Append("@{\r\n\tLayout = "); + if (layoutPageAlias.IsNullOrWhiteSpace()) + { + content.Append("null"); + } + else + { + content.Append("\""); + content.Append(layoutPageAlias); + content.Append(".cshtml\""); + } + content.Append(";\r\n}"); + return content.ToString(); } private string SaveTemplateToFile(ITemplate template) diff --git a/src/Umbraco.Core/Macros/MacroTagParser.cs b/src/Umbraco.Core/Macros/MacroTagParser.cs index 850e3845af..66f8a8a568 100644 --- a/src/Umbraco.Core/Macros/MacroTagParser.cs +++ b/src/Umbraco.Core/Macros/MacroTagParser.cs @@ -11,8 +11,12 @@ namespace Umbraco.Core.Macros /// internal class MacroTagParser { - private static readonly Regex MacroRteContent = new Regex(@"()", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline); - private static readonly Regex MacroPersistedFormat = new Regex(@"(<\?UMBRACO_MACRO (?:.+?)?macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex MacroRteContent = new Regex(@"()", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); + + private static readonly Regex MacroPersistedFormat = + new Regex(@"(<\?UMBRACO_MACRO (?:.+?)??macroAlias=[""']([^""\'\n\r]+?)[""'].+?)(?:/>|>.*?)", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Singleline); /// /// This formats the persisted string to something useful for the rte so that the macro renders properly since we diff --git a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs index 452c05adb8..1be504ea45 100644 --- a/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs +++ b/src/Umbraco.Core/Media/Exif/ExifPropertyCollection.cs @@ -75,7 +75,7 @@ namespace Umbraco.Core.Media.Exif { if (items.ContainsKey (key)) items.Remove (key); - if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { + if (key == ExifTag.WindowsTitle || key == ExifTag.WindowsComment || key == ExifTag.WindowsAuthor || key == ExifTag.WindowsKeywords || key == ExifTag.WindowsSubject) { items.Add (key, new WindowsByteString (key, value)); } else { items.Add (key, new ExifAscii (key, value, parent.Encoding)); diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs index 5842bb67bb..99fc278e61 100644 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ b/src/Umbraco.Core/Media/ImageHelper.cs @@ -59,7 +59,7 @@ namespace Umbraco.Core.Media var fileHeight = image.Height; return new Size(fileWidth, fileHeight); } - + } public static string GetMimeType(this Image image) @@ -79,22 +79,22 @@ namespace Umbraco.Core.Media /// /// internal static IEnumerable GenerateMediaThumbnails( - IFileSystem fs, - string fileName, - string extension, + IFileSystem fs, + string fileName, + string extension, Image originalImage, IEnumerable additionalThumbSizes) { var result = new List(); - var allSizesDictionary = new Dictionary {{100,"thumb"}, {500,"big-thumb"}}; - + var allSizesDictionary = new Dictionary { { 100, "thumb" }, { 500, "big-thumb" } }; + //combine the static dictionary with the additional sizes with only unique values var allSizes = allSizesDictionary.Select(kv => kv.Key) .Union(additionalThumbSizes.Where(x => x > 0).Distinct()); - var sizesDictionary = allSizes.ToDictionary(s => s, s => allSizesDictionary.ContainsKey(s) ? allSizesDictionary[s]: ""); + var sizesDictionary = allSizes.ToDictionary(s => s, s => allSizesDictionary.ContainsKey(s) ? allSizesDictionary[s] : ""); foreach (var s in sizesDictionary) { @@ -121,9 +121,9 @@ namespace Umbraco.Core.Media /// private static ResizedImage Resize(IFileSystem fileSystem, string path, string extension, int maxWidthHeight, string fileNameAddition, Image originalImage) { - var fileNameThumb = String.IsNullOrEmpty(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL.jpg", path.Substring(0, path.LastIndexOf("."))) - : string.Format("{0}_{1}.jpg", path.Substring(0, path.LastIndexOf(".")), fileNameAddition); + var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) + ? string.Format("{0}_UMBRACOSYSTHUMBNAIL." + extension, path.Substring(0, path.LastIndexOf(".", StringComparison.Ordinal))) + : string.Format("{0}_{1}." + extension, path.Substring(0, path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); var thumb = GenerateThumbnail( originalImage, @@ -190,9 +190,9 @@ namespace Umbraco.Core.Media //use best quality g.InterpolationMode = InterpolationMode.HighQualityBicubic; } - - g.SmoothingMode = SmoothingMode.HighQuality; + + g.SmoothingMode = SmoothingMode.HighQuality; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.CompositingQuality = CompositingQuality.HighQuality; @@ -202,10 +202,29 @@ namespace Umbraco.Core.Media // Copy metadata var imageEncoders = ImageCodecInfo.GetImageEncoders(); - - var codec = extension.ToLower() == "png" || extension.ToLower() == "gif" - ? imageEncoders.Single(t => t.MimeType.Equals("image/png")) - : imageEncoders.Single(t => t.MimeType.Equals("image/jpeg")); + ImageCodecInfo codec; + switch (extension.ToLower()) + { + case "png": + codec = imageEncoders.Single(t => t.MimeType.Equals("image/png")); + break; + case "gif": + codec = imageEncoders.Single(t => t.MimeType.Equals("image/gif")); + break; + case "tif": + case "tiff": + codec = imageEncoders.Single(t => t.MimeType.Equals("image/tiff")); + break; + case "bmp": + codec = imageEncoders.Single(t => t.MimeType.Equals("image/bmp")); + break; + // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the + // place so left it here, but it needs to not set a codec if it doesn't know which one to pick + // Note: when fixing this: both .jpg and .jpeg should be handled as extensions + default: + codec = imageEncoders.Single(t => t.MimeType.Equals("image/jpeg")); + break; + } // Set compresion ratio to 90% var ep = new EncoderParameters(); @@ -213,12 +232,14 @@ namespace Umbraco.Core.Media // Save the new image using the dimensions of the image var predictableThumbnailName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToString(CultureInfo.InvariantCulture)); + var predictableThumbnailNameJpg = predictableThumbnailName.Substring(0, predictableThumbnailName.LastIndexOf(".", StringComparison.Ordinal)) + ".jpg"; using (var ms = new MemoryStream()) { bp.Save(ms, codec, ep); ms.Seek(0, 0); fs.AddFile(predictableThumbnailName, ms); + fs.AddFile(predictableThumbnailNameJpg, ms); } // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 2238260611..2b20b68164 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.Serialization; @@ -155,6 +156,7 @@ namespace Umbraco.Core.Models /// Language of the data contained within this Content object. /// [Obsolete("This is not used and will be removed from the codebase in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public string Language { get { return _language; } @@ -340,18 +342,6 @@ namespace Umbraco.Core.Models } } - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if(Key == Guid.Empty) - Key = Guid.NewGuid(); - } - /// /// Method to call when Entity is being updated /// diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 6beead06e6..b3d0f693d9 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -367,8 +367,27 @@ namespace Umbraco.Core.Models /// Value to set for the Property public virtual void SetPropertyValue(string propertyTypeAlias, long value) { - string val = value.ToString(); - SetValueOnProperty(propertyTypeAlias, val); + SetValueOnProperty(propertyTypeAlias, value); + } + + /// + /// Sets the value of a Property + /// + /// Alias of the PropertyType + /// Value to set for the Property + public virtual void SetPropertyValue(string propertyTypeAlias, decimal value) + { + SetValueOnProperty(propertyTypeAlias, value); + } + + /// + /// Sets the value of a Property + /// + /// Alias of the PropertyType + /// Value to set for the Property + public virtual void SetPropertyValue(string propertyTypeAlias, double value) + { + SetValueOnProperty(propertyTypeAlias, value); } /// diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 88b6308c29..e91996e32a 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -560,8 +560,7 @@ namespace Umbraco.Core.Models //Additional thumbnails configured as prevalues on the DataType if (thumbnailSizes != null) { - var sep = (thumbnailSizes.Contains("") == false && thumbnailSizes.Contains(",")) ? ',' : ';'; - foreach (var thumb in thumbnailSizes.Split(sep)) + foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) { int thumbSize; if (thumb != "" && int.TryParse(thumb, out thumbSize)) diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 355d724fbe..0926e48e31 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -53,6 +53,9 @@ namespace Umbraco.Core.Models /// /// Gets or sets the alias of the default Template. + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity /// [IgnoreDataMember] public ITemplate DefaultTemplate @@ -79,6 +82,9 @@ namespace Umbraco.Core.Models /// /// Gets or Sets a list of Templates which are allowed for the ContentType + /// TODO: This should be ignored from cloning!!!!!!!!!!!!!! + /// - but to do that we have to implement callback hacks, this needs to be fixed in v8, + /// we should not store direct entity /// [DataMember] public IEnumerable AllowedTemplates @@ -137,19 +143,6 @@ namespace Umbraco.Core.Models return result; } - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - - /// /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs new file mode 100644 index 0000000000..b1d2b45dc5 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResult + { + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) + { + Composition = composition; + Allowed = allowed; + } + + public IContentTypeComposition Composition { get; private set; } + public bool Allowed { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs new file mode 100644 index 0000000000..653d7a10a9 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResults + { + public ContentTypeAvailableCompositionsResults() + { + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); + } + + public ContentTypeAvailableCompositionsResults(IEnumerable ancestors, IEnumerable results) + { + Ancestors = ancestors; + Results = results; + } + + public IEnumerable Ancestors { get; private set; } + public IEnumerable Results { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index c1c8e3270c..a0305d2cfb 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Models private string _alias; private string _description; private int _sortOrder; - private string _icon = "folder.png"; + private string _icon = "icon-folder"; private string _thumbnail = "folder.png"; private int _creatorId; private bool _allowedAsRoot; @@ -373,43 +373,35 @@ namespace Umbraco.Core.Models { _propertyGroups = value; _propertyGroups.CollectionChanged += PropertyGroupsChanged; + PropertyGroupsChanged(_propertyGroups, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } /// - /// List of PropertyTypes available on this ContentType. - /// This list aggregates PropertyTypes across the PropertyGroups. + /// Gets all property types, across all property groups. /// - /// - /// - /// The setter is used purely to set the property types that DO NOT belong to a group! - /// - /// Marked as DoNotClone because the result of this property is not the natural result of the data, it is - /// a union of data so when auto-cloning if the setter is used it will be setting the unnatural result of the - /// data. We manually clone this instead. - /// [IgnoreDataMember] [DoNotClone] public virtual IEnumerable PropertyTypes { get { - var types = _propertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes)); - return types; - } - internal set - { - _propertyTypes = new PropertyTypeCollection(value); - _propertyTypes.CollectionChanged += PropertyTypesChanged; + return _propertyTypes.Union(PropertyGroups.SelectMany(x => x.PropertyTypes)); } } /// - /// Returns the property type collection containing types that are non-groups - used for tests + /// Gets or sets the property types that are not in a group. /// - internal IEnumerable NonGroupedPropertyTypes + public IEnumerable NoGroupPropertyTypes { get { return _propertyTypes; } + set + { + _propertyTypes = new PropertyTypeCollection(value); + _propertyTypes.CollectionChanged += PropertyTypesChanged; + PropertyTypesChanged(_propertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } } /// @@ -460,11 +452,6 @@ namespace Umbraco.Core.Models /// Returns True if PropertyType was added, otherwise False public bool AddPropertyType(PropertyType propertyType) { - if (propertyType.HasIdentity == false) - { - propertyType.Key = Guid.NewGuid(); - } - if (PropertyTypeExists(propertyType.Alias) == false) { _propertyTypes.Add(propertyType); @@ -480,24 +467,34 @@ namespace Umbraco.Core.Models /// Alias of the PropertyType to move /// Name of the PropertyGroup to move the PropertyType to /// + /// If is null then the property is moved back to + /// "generic properties" ie does not have a tab anymore. public bool MovePropertyType(string propertyTypeAlias, string propertyGroupName) { - if (PropertyTypes.Any(x => x.Alias == propertyTypeAlias) == false || PropertyGroups.Any(x => x.Name == propertyGroupName) == false) - return false; + // note: not dealing with alias casing at all here? - var propertyType = PropertyTypes.First(x => x.Alias == propertyTypeAlias); - //The PropertyType already belongs to a PropertyGroup, so we have to remove the PropertyType from that group - if (PropertyGroups.Any(x => x.PropertyTypes.Any(y => y.Alias == propertyTypeAlias))) - { - var oldPropertyGroup = PropertyGroups.First(x => x.PropertyTypes.Any(y => y.Alias == propertyTypeAlias)); + // get property, ensure it exists + var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias); + if (propertyType == null) return false; + + // get new group, if required, and ensure it exists + var newPropertyGroup = propertyGroupName == null + ? null + : PropertyGroups.FirstOrDefault(x => x.Name == propertyGroupName); + if (propertyGroupName != null && newPropertyGroup == null) return false; + + // get old group + var oldPropertyGroup = PropertyGroups.FirstOrDefault(x => + x.PropertyTypes.Any(y => y.Alias == propertyTypeAlias)); + + // set new group + propertyType.PropertyGroupId = newPropertyGroup == null ? null : new Lazy(() => newPropertyGroup.Id, false); + + // remove from old group, if any - add to new group, if any + if (oldPropertyGroup != null) oldPropertyGroup.PropertyTypes.RemoveItem(propertyTypeAlias); - } - - propertyType.PropertyGroupId = new Lazy(() => default(int)); - propertyType.ResetDirtyProperties(); - - var propertyGroup = PropertyGroups.First(x => x.Name == propertyGroupName); - propertyGroup.PropertyTypes.Add(propertyType); + if (newPropertyGroup != null) + newPropertyGroup.PropertyTypes.Add(propertyType); return true; } @@ -533,6 +530,18 @@ namespace Umbraco.Core.Models /// Name of the to remove public void RemovePropertyGroup(string propertyGroupName) { + // if no group exists with that name, do nothing + var group = PropertyGroups[propertyGroupName]; + if (group == null) return; + + // re-assign the group's properties to no group + foreach (var property in group.PropertyTypes) + { + property.PropertyGroupId = null; + _propertyTypes.Add(property); + } + + // actually remove the group PropertyGroups.RemoveItem(propertyGroupName); OnPropertyChanged(PropertyGroupCollectionSelector); } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index d102f06483..4cf4a08bf1 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -23,8 +23,8 @@ namespace Umbraco.Core.Models protected ContentTypeCompositionBase(IContentTypeComposition parent) : this(parent, null) - { - } + { + } protected ContentTypeCompositionBase(IContentTypeComposition parent, string alias) : base(parent, alias) @@ -37,16 +37,21 @@ namespace Umbraco.Core.Models x => x.ContentTypeComposition); /// - /// List of ContentTypes that make up a composition of PropertyGroups and PropertyTypes for the current ContentType + /// Gets or sets the content types that compose this content type. /// [DataMember] public IEnumerable ContentTypeComposition { get { return _contentTypeComposition; } + set + { + _contentTypeComposition = value.ToList(); + OnPropertyChanged(ContentTypeCompositionSelector); + } } /// - /// Returns a list of objects from the composition + /// Gets the property groups for the entire composition. /// [IgnoreDataMember] public IEnumerable CompositionPropertyGroups @@ -59,7 +64,7 @@ namespace Umbraco.Core.Models } /// - /// Returns a list of objects from the composition + /// Gets the property types for the entire composition. /// [IgnoreDataMember] public IEnumerable CompositionPropertyTypes @@ -72,10 +77,10 @@ namespace Umbraco.Core.Models } /// - /// Adds a new ContentType to the list of composite ContentTypes + /// Adds a content type to the composition. /// - /// to add - /// True if ContentType was added, otherwise returns False + /// The content type to add. + /// True if the content type was added, otherwise false. public bool AddContentType(IContentTypeComposition contentType) { if (contentType.ContentTypeComposition.Any(x => x.CompositionAliases().Any(ContentTypeCompositionExists))) @@ -94,13 +99,7 @@ namespace Umbraco.Core.Models .Select(p => p.Alias)).ToList(); if (conflictingPropertyTypeAliases.Any()) - throw new InvalidCompositionException - { - AddedCompositionAlias = contentType.Alias, - ContentTypeAlias = Alias, - PropertyTypeAlias = - string.Join(", ", conflictingPropertyTypeAliases) - }; + throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); _contentTypeComposition.Add(contentType); OnPropertyChanged(ContentTypeCompositionSelector); @@ -110,10 +109,10 @@ namespace Umbraco.Core.Models } /// - /// Removes a ContentType with the supplied alias from the the list of composite ContentTypes + /// Removes a content type with a specified alias from the composition. /// - /// Alias of a - /// True if ContentType was removed, otherwise returns False + /// The alias of the content type to remove. + /// True if the content type was removed, otherwise false. public bool RemoveContentType(string alias) { if (ContentTypeCompositionExists(alias)) @@ -123,10 +122,10 @@ namespace Umbraco.Core.Models return false; RemovedContentTypeKeyTracker.Add(contentTypeComposition.Id); - + //If the ContentType we are removing has Compositions of its own these needs to be removed as well var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); - if(compositionIdsToRemove.Any()) + if (compositionIdsToRemove.Any()) RemovedContentTypeKeyTracker.AddRange(compositionIdsToRemove); OnPropertyChanged(ContentTypeCompositionSelector); @@ -163,31 +162,44 @@ namespace Umbraco.Core.Models /// /// Adds a PropertyGroup. - /// This method will also check if a group already exists with the same name and link it to the parent. /// /// Name of the PropertyGroup to add /// Returns True if a PropertyGroup with the passed in name was added, otherwise False public override bool AddPropertyGroup(string groupName) { - if (PropertyGroups.Any(x => x.Name == groupName)) - return false; + return AddAndReturnPropertyGroup(groupName) != null; + } - var propertyGroup = new PropertyGroup {Name = groupName, SortOrder = 0}; + private PropertyGroup AddAndReturnPropertyGroup(string name) + { + // ensure we don't have it already + if (PropertyGroups.Any(x => x.Name == name)) + return null; - if (CompositionPropertyGroups.Any(x => x.Name == groupName)) + // create the new group + var group = new PropertyGroup { Name = name, SortOrder = 0 }; + + // check if it is inherited - there might be more than 1 but we want the 1st, to + // reuse its sort order - if there are more than 1 and they have different sort + // orders... there isn't much we can do anyways + var inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Name == name); + if (inheritGroup == null) { - var firstGroup = CompositionPropertyGroups.First(x => x.Name == groupName && x.ParentId.HasValue == false); - propertyGroup.SetLazyParentId(new Lazy(() => firstGroup.Id)); + // no, just local, set sort order + var lastGroup = PropertyGroups.LastOrDefault(); + if (lastGroup != null) + group.SortOrder = lastGroup.SortOrder + 1; + } + else + { + // yes, inherited, re-use sort order + group.SortOrder = inheritGroup.SortOrder; } - if (PropertyGroups.Any()) - { - var last = PropertyGroups.Last(); - propertyGroup.SortOrder = last.SortOrder + 1; - } + // add + PropertyGroups.Add(group); - PropertyGroups.Add(propertyGroup); - return true; + return group; } /// @@ -198,39 +210,22 @@ namespace Umbraco.Core.Models /// Returns True if PropertyType was added, otherwise False public override bool AddPropertyType(PropertyType propertyType, string propertyGroupName) { - if (propertyType.HasIdentity == false) - { - propertyType.Key = Guid.NewGuid(); - } + // ensure no duplicate alias - over all composition properties + if (PropertyTypeExists(propertyType.Alias)) + return false; - if (PropertyTypeExists(propertyType.Alias) == false) - { - if (PropertyGroups.Contains(propertyGroupName)) - { - propertyType.PropertyGroupId = new Lazy(() => PropertyGroups[propertyGroupName].Id); - PropertyGroups[propertyGroupName].PropertyTypes.Add(propertyType); - } - else - { - //If the PropertyGroup doesn't already exist we create a new one - var propertyTypes = new List { propertyType }; - var propertyGroup = new PropertyGroup(new PropertyTypeCollection(propertyTypes)) { Name = propertyGroupName, SortOrder = 1 }; - //and check if its an inherited PropertyGroup, which exists in the composition - if (CompositionPropertyGroups.Any(x => x.Name == propertyGroupName)) - { - var parentPropertyGroup = CompositionPropertyGroups.First(x => x.Name == propertyGroupName && x.ParentId.HasValue == false); - propertyGroup.SortOrder = parentPropertyGroup.SortOrder; - //propertyGroup.ParentId = parentPropertyGroup.Id; - propertyGroup.SetLazyParentId(new Lazy(() => parentPropertyGroup.Id)); - } + // get and ensure a group local to this content type + var group = PropertyGroups.Contains(propertyGroupName) + ? PropertyGroups[propertyGroupName] + : AddAndReturnPropertyGroup(propertyGroupName); + if (group == null) + return false; - PropertyGroups.Add(propertyGroup); - } + // add property to group + propertyType.PropertyGroupId = new Lazy(() => group.Id); + group.PropertyTypes.Add(propertyType); - return true; - } - - return false; + return true; } /// diff --git a/src/Umbraco.Core/Models/DataTypeDatabaseType.cs b/src/Umbraco.Core/Models/DataTypeDatabaseType.cs index e45cdd1a73..2fccdc0645 100644 --- a/src/Umbraco.Core/Models/DataTypeDatabaseType.cs +++ b/src/Umbraco.Core/Models/DataTypeDatabaseType.cs @@ -6,10 +6,6 @@ namespace Umbraco.Core.Models /// /// Enum of the various DbTypes for which the Property values are stored /// - /// - /// Object is added to support complex values from PropertyEditors, - /// but will be saved under the Ntext column. - /// [Serializable] [DataContract] public enum DataTypeDatabaseType @@ -21,6 +17,8 @@ namespace Umbraco.Core.Models [EnumMember] Integer, [EnumMember] - Date + Date, + [EnumMember] + Decimal } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/DataTypeDefinition.cs b/src/Umbraco.Core/Models/DataTypeDefinition.cs index 5d5b9490a5..4c12d6fbef 100644 --- a/src/Umbraco.Core/Models/DataTypeDefinition.cs +++ b/src/Umbraco.Core/Models/DataTypeDefinition.cs @@ -49,8 +49,6 @@ namespace Umbraco.Core.Models _additionalData = new Dictionary(); } - [Obsolete("Don't use this, parentId is always -1 for data types")] - [EditorBrowsable(EditorBrowsableState.Never)] public DataTypeDefinition(int parentId, string propertyEditorAlias) { _parentId = parentId; @@ -267,13 +265,5 @@ namespace Umbraco.Core.Models { get { return _additionalData; } } - - internal override void AddingEntity() - { - base.AddingEntity(); - - if(Key == default(Guid)) - Key = Guid.NewGuid(); - } } } diff --git a/src/Umbraco.Core/Models/DeepCloneHelper.cs b/src/Umbraco.Core/Models/DeepCloneHelper.cs index 0e0e260c06..c1b45f63ce 100644 --- a/src/Umbraco.Core/Models/DeepCloneHelper.cs +++ b/src/Umbraco.Core/Models/DeepCloneHelper.cs @@ -9,10 +9,30 @@ namespace Umbraco.Core.Models { public static class DeepCloneHelper { + /// + /// Stores the metadata for the properties for a given type so we know how to create them + /// + private struct ClonePropertyInfo + { + public ClonePropertyInfo(PropertyInfo propertyInfo) : this() + { + if (propertyInfo == null) throw new ArgumentNullException("propertyInfo"); + PropertyInfo = propertyInfo; + } + + public PropertyInfo PropertyInfo { get; private set; } + public bool IsDeepCloneable { get; set; } + public Type GenericListType { get; set; } + public bool IsList + { + get { return GenericListType != null; } + } + } + /// /// Used to avoid constant reflection (perf) /// - private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary PropCache = new ConcurrentDictionary(); /// /// Used to deep clone any reference properties on the object (should be done after a MemberwiseClone for which the outcome is 'output') @@ -30,75 +50,99 @@ namespace Umbraco.Core.Models throw new InvalidOperationException("Both the input and output types must be the same"); } + //get the property metadata from cache so we only have to figure this out once per type var refProperties = PropCache.GetOrAdd(inputType, type => inputType.GetProperties() - .Where(x => - //is not attributed with the ignore clone attribute - x.GetCustomAttribute() == null + .Select(propertyInfo => + { + if ( + //is not attributed with the ignore clone attribute + propertyInfo.GetCustomAttribute() != null //reference type but not string - && x.PropertyType.IsValueType == false && x.PropertyType != typeof (string) + || propertyInfo.PropertyType.IsValueType || propertyInfo.PropertyType == typeof (string) //settable - && x.CanWrite + || propertyInfo.CanWrite == false //non-indexed - && x.GetIndexParameters().Any() == false) + || propertyInfo.GetIndexParameters().Any()) + { + return null; + } + + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + { + return new ClonePropertyInfo(propertyInfo) { IsDeepCloneable = true }; + } + + if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) + && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + { + if (propertyInfo.PropertyType.IsGenericType + && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) + || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) + { + //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> + var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); + return new ClonePropertyInfo(propertyInfo) { GenericListType = genericType }; + } + if (propertyInfo.PropertyType.IsArray + || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) + { + //if its an array, we'll create a list to work with first and then convert to array later + //otherwise if its just a regular derivitave of IEnumerable, we can use a list too + return new ClonePropertyInfo(propertyInfo) { GenericListType = typeof(List) }; + } + //skip instead of trying to create instance of abstract or interface + if (propertyInfo.PropertyType.IsAbstract || propertyInfo.PropertyType.IsInterface) + { + return null; + } + + //its a custom IEnumerable, we'll try to create it + try + { + var custom = Activator.CreateInstance(propertyInfo.PropertyType); + //if it's an IList we can work with it, otherwise we cannot + var newList = custom as IList; + if (newList == null) + { + return null; + } + return new ClonePropertyInfo(propertyInfo) {GenericListType = propertyInfo.PropertyType}; + } + catch (Exception) + { + //could not create this type so we'll skip it + return null; + } + } + return new ClonePropertyInfo(propertyInfo); + }) + .Where(x => x.HasValue) + .Select(x => x.Value) .ToArray()); - foreach (var propertyInfo in refProperties) + foreach (var clonePropertyInfo in refProperties) { - if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType)) + if (clonePropertyInfo.IsDeepCloneable) { //this ref property is also deep cloneable so clone it - var result = (IDeepCloneable)propertyInfo.GetValue(input, null); + var result = (IDeepCloneable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (result != null) { //set the cloned value to the property - propertyInfo.SetValue(output, result.DeepClone(), null); + clonePropertyInfo.PropertyInfo.SetValue(output, result.DeepClone(), null); } } - else if (TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) - && TypeHelper.IsTypeAssignableFrom(propertyInfo.PropertyType) == false) + else if (clonePropertyInfo.IsList) { - IList newList; - if (propertyInfo.PropertyType.IsGenericType - && (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IEnumerable<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>) - || propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))) - { - //if it is a IEnumerable<>, IList or ICollection<> we'll use a List<> - var genericType = typeof(List<>).MakeGenericType(propertyInfo.PropertyType.GetGenericArguments()); - newList = (IList)Activator.CreateInstance(genericType); - } - else if (propertyInfo.PropertyType.IsArray - || (propertyInfo.PropertyType.IsInterface && propertyInfo.PropertyType.IsGenericType == false)) - { - //if its an array, we'll create a list to work with first and then convert to array later - //otherwise if its just a regular derivitave of IEnumerable, we can use a list too - newList = new List(); - } - else - { - //its a custom IEnumerable, we'll try to create it - try - { - var custom = Activator.CreateInstance(propertyInfo.PropertyType); - //if it's an IList we can work with it, otherwise we cannot - newList = custom as IList; - if (newList == null) - { - continue; - } - } - catch (Exception) - { - //could not create this type so we'll skip it - continue; - } - } - - var enumerable = (IEnumerable)propertyInfo.GetValue(input, null); + var enumerable = (IEnumerable)clonePropertyInfo.PropertyInfo.GetValue(input, null); if (enumerable == null) continue; + var newList = (IList)Activator.CreateInstance(clonePropertyInfo.GenericListType); + var isUsableType = true; //now clone each item @@ -130,21 +174,21 @@ namespace Umbraco.Core.Models continue; } - if (propertyInfo.PropertyType.IsArray) + if (clonePropertyInfo.PropertyInfo.PropertyType.IsArray) { //need to convert to array - var arr = (object[])Activator.CreateInstance(propertyInfo.PropertyType, newList.Count); + var arr = (object[])Activator.CreateInstance(clonePropertyInfo.PropertyInfo.PropertyType, newList.Count); for (int i = 0; i < newList.Count; i++) { arr[i] = newList[i]; } //set the cloned collection - propertyInfo.SetValue(output, arr, null); + clonePropertyInfo.PropertyInfo.SetValue(output, arr, null); } else { //set the cloned collection - propertyInfo.SetValue(output, newList, null); + clonePropertyInfo.PropertyInfo.SetValue(output, newList, null); } } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 7e59726c80..749c629d19 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -14,6 +14,7 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class DictionaryItem : Entity, IDictionaryItem { + public Func GetLanguage { get; set; } private Guid? _parentId; private string _itemKey; private IEnumerable _translations; @@ -78,7 +79,17 @@ namespace Umbraco.Core.Models { SetPropertyValueAndDetectChanges(o => { - _translations = value; + var asArray = value.ToArray(); + //ensure the language callback is set on each translation + if (GetLanguage != null) + { + foreach (var translation in asArray.OfType()) + { + translation.GetLanguage = GetLanguage; + } + } + + _translations = asArray; return _translations; }, _translations, TranslationsSelector, //Custom comparer for enumerable @@ -87,16 +98,5 @@ namespace Umbraco.Core.Models enumerable => enumerable.GetHashCode())); } } - - /// - /// Method to call before inserting a new entity in the db - /// - internal override void AddingEntity() - { - base.AddingEntity(); - - Key = Guid.NewGuid(); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/DictionaryTranslation.cs b/src/Umbraco.Core/Models/DictionaryTranslation.cs index 782ff14413..59f96dbe85 100644 --- a/src/Umbraco.Core/Models/DictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/DictionaryTranslation.cs @@ -13,13 +13,18 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class DictionaryTranslation : Entity, IDictionaryTranslation { + internal Func GetLanguage { get; set; } + private ILanguage _language; private string _value; + //note: this will be memberwise cloned + private int _languageId; public DictionaryTranslation(ILanguage language, string value) { if (language == null) throw new ArgumentNullException("language"); _language = language; + _languageId = _language.Id; _value = value; } @@ -27,6 +32,20 @@ namespace Umbraco.Core.Models { if (language == null) throw new ArgumentNullException("language"); _language = language; + _languageId = _language.Id; + _value = value; + Key = uniqueId; + } + + internal DictionaryTranslation(int languageId, string value) + { + _languageId = languageId; + _value = value; + } + + internal DictionaryTranslation(int languageId, string value, Guid uniqueId) + { + _languageId = languageId; _value = value; Key = uniqueId; } @@ -37,20 +56,43 @@ namespace Umbraco.Core.Models /// /// Gets or sets the for the translation /// + /// + /// Marked as DoNotClone - TODO: this member shouldn't really exist here in the first place, the DictionaryItem + /// class will have a deep hierarchy of objects which all get deep cloned which we don't want. This should have simply + /// just referenced a language ID not the actual language object. In v8 we need to fix this. + /// We're going to have to do the same hacky stuff we had to do with the Template/File contents so that this is returned + /// on a callback. + /// [DataMember] + [DoNotClone] public ILanguage Language { - get { return _language; } + get + { + if (_language != null) + return _language; + + // else, must lazy-load + if (GetLanguage != null && _languageId > 0) + _language = GetLanguage(_languageId); + return _language; + } set { SetPropertyValueAndDetectChanges(o => { _language = value; + _languageId = _language == null ? -1 : _language.Id; return _language; }, _language, LanguageSelector); } } + public int LanguageId + { + get { return _languageId; } + } + /// /// Gets or sets the translated text /// @@ -68,5 +110,23 @@ namespace Umbraco.Core.Models } } + public override object DeepClone() + { + var clone = (DictionaryTranslation)base.DeepClone(); + + // clear fields that were memberwise-cloned and that we don't want to clone + clone._language = null; + + // turn off change tracking + clone.DisableChangeTracking(); + + // this shouldn't really be needed since we're not tracking + clone.ResetDirtyProperties(false); + + // re-enable tracking + clone.EnableChangeTracking(); + + return clone; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index eeacb771a9..c4838dfd0a 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -62,9 +62,9 @@ namespace Umbraco.Core.Models.EntityBase { get { + // if an entity does NOT have a UniqueId yet, assign one now if (_key == Guid.Empty) - return _id.ToGuid(); - + _key = Guid.NewGuid(); return _key; } set @@ -136,6 +136,7 @@ namespace Umbraco.Core.Models.EntityBase { _hasIdentity = false; _id = default(int); + _key = Guid.Empty; } /// @@ -242,6 +243,7 @@ namespace Umbraco.Core.Models.EntityBase { //Memberwise clone on Entity will work since it doesn't have any deep elements // for any sub class this will work for standard properties as well that aren't complex object's themselves. + var ignored = this.Key; // ensure that 'this' has a key, before cloning var clone = (Entity)MemberwiseClone(); //ensure the clone has it's own dictionaries clone.ResetChangeTrackingCollections(); diff --git a/src/Umbraco.Core/Models/EntityContainer.cs b/src/Umbraco.Core/Models/EntityContainer.cs new file mode 100644 index 0000000000..92965bf238 --- /dev/null +++ b/src/Umbraco.Core/Models/EntityContainer.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a folder for organizing entities such as content types and data types. + /// + public sealed class EntityContainer : UmbracoEntity + { + private readonly Guid _containedObjectType; + + private static readonly Dictionary ObjectTypeMap = new Dictionary + { + { Constants.ObjectTypes.DataTypeGuid, Constants.ObjectTypes.DataTypeContainerGuid }, + { Constants.ObjectTypes.DocumentTypeGuid, Constants.ObjectTypes.DocumentTypeContainerGuid }, + { Constants.ObjectTypes.MediaTypeGuid, Constants.ObjectTypes.MediaTypeContainerGuid } + }; + + /// + /// Initializes a new instance of an class. + /// + public EntityContainer(Guid containedObjectType) + { + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) + throw new ArgumentException("Not a contained object type.", "containedObjectType"); + _containedObjectType = containedObjectType; + + ParentId = -1; + Path = "-1"; + Level = 0; + SortOrder = 0; + } + + /// + /// Initializes a new instance of an class. + /// + internal EntityContainer(int id, Guid uniqueId, int parentId, string path, int level, int sortOrder, Guid containedObjectType, string name, int userId) + : this(containedObjectType) + { + Id = id; + Key = uniqueId; + ParentId = parentId; + Name = name; + Path = path; + Level = level; + SortOrder = sortOrder; + CreatorId = userId; + } + + /// + /// Gets or sets the node object type of the contained objects. + /// + public Guid ContainedObjectType + { + get { return _containedObjectType; } + } + + /// + /// Gets the node object type of the container objects. + /// + public Guid ContainerObjectType + { + get { return ObjectTypeMap[_containedObjectType]; } + } + + /// + /// Gets the container object type corresponding to a contained object type. + /// + /// The contained object type. + /// The object type of containers containing objects of the contained object type. + public static Guid GetContainerObjectType(Guid containedObjectType) + { + if (ObjectTypeMap.ContainsKey(containedObjectType) == false) + throw new ArgumentException("Not a contained object type.", "containedObjectType"); + return ObjectTypeMap[containedObjectType]; + } + + /// + /// Gets the contained object type corresponding to a container object type. + /// + /// The container object type. + /// The object type of objects that containers of the container object type can contain. + public static Guid GetContainedObjectType(Guid containerObjectType) + { + var contained = ObjectTypeMap.FirstOrDefault(x => x.Value == containerObjectType).Key; + if (contained == null) + throw new ArgumentException("Not a container object type.", "containerObjectType"); + return contained; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 2715e5fe3a..7caefc1121 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics; using Umbraco.Core.Persistence.Mappers; @@ -21,6 +22,7 @@ namespace Umbraco.Core.Models bool Published { get; } [Obsolete("This will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] string Language { get; set; } /// diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index 89fca20b8e..80e62f50cf 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -54,10 +54,15 @@ namespace Umbraco.Core.Models PropertyGroupCollection PropertyGroups { get; set; } /// - /// Gets an enumerable list of Property Types aggregated for all groups + /// Gets all property types, across all property groups. /// IEnumerable PropertyTypes { get; } + /// + /// Gets or sets the property types that are not in a group. + /// + IEnumerable NoGroupPropertyTypes { get; set; } + /// /// Removes a PropertyType from the current ContentType /// diff --git a/src/Umbraco.Core/Models/IContentTypeComposition.cs b/src/Umbraco.Core/Models/IContentTypeComposition.cs index 8b5049f236..ab7e068fdd 100644 --- a/src/Umbraco.Core/Models/IContentTypeComposition.cs +++ b/src/Umbraco.Core/Models/IContentTypeComposition.cs @@ -8,17 +8,17 @@ namespace Umbraco.Core.Models public interface IContentTypeComposition : IContentTypeBase { /// - /// Gets a list of ContentTypes that make up a composition of PropertyGroups and PropertyTypes for the current ContentType + /// Gets or sets the content types that compose this content type. /// - IEnumerable ContentTypeComposition { get; } + IEnumerable ContentTypeComposition { get; set; } /// - /// Gets a list of objects from the composition + /// Gets the property groups for the entire composition. /// IEnumerable CompositionPropertyGroups { get; } /// - /// Gets a list of objects from the composition + /// Gets the property types for the entire composition. /// IEnumerable CompositionPropertyTypes { get; } diff --git a/src/Umbraco.Core/Models/IDictionaryTranslation.cs b/src/Umbraco.Core/Models/IDictionaryTranslation.cs index cf813bf72f..25aa1e4395 100644 --- a/src/Umbraco.Core/Models/IDictionaryTranslation.cs +++ b/src/Umbraco.Core/Models/IDictionaryTranslation.cs @@ -12,6 +12,8 @@ namespace Umbraco.Core.Models [DataMember] ILanguage Language { get; set; } + int LanguageId { get; } + /// /// Gets or sets the translated text /// diff --git a/src/Umbraco.Core/Models/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 3934d7a40f..29e4b665ba 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -7,6 +7,12 @@ namespace Umbraco.Core.Models /// + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset + /// + /// + /// + IMediaType DeepCloneWithResetIdentities(string newAlias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index f57d6683a2..0dc95a8987 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -4,6 +4,7 @@ using AutoMapper; using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; namespace Umbraco.Core.Models.Identity { @@ -24,6 +25,18 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) .ForMember(user => user.AccessFailedCount, expression => expression.MapFrom(user => user.FailedPasswordAttempts)) .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); + + config.CreateMap() + .ConstructUsing((BackOfficeIdentityUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' + .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) + .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] { user.UserTypeAlias })) + .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.UserName)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) + .ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); } private string GetPasswordHash(string storedPass) diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs index ef8ebbf931..b23bbfb52a 100644 --- a/src/Umbraco.Core/Models/Language.cs +++ b/src/Umbraco.Core/Models/Language.cs @@ -64,7 +64,7 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public CultureInfo CultureInfo { - get { return CultureInfo.CreateSpecificCulture(IsoCode); } + get { return CultureInfo.GetCultureInfo(IsoCode); } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Mapping/MappingExpressionExtensions.cs b/src/Umbraco.Core/Models/Mapping/MappingExpressionExtensions.cs new file mode 100644 index 0000000000..570e51dbc3 --- /dev/null +++ b/src/Umbraco.Core/Models/Mapping/MappingExpressionExtensions.cs @@ -0,0 +1,20 @@ +using AutoMapper; + +namespace Umbraco.Core.Models.Mapping +{ + internal static class MappingExpressionExtensions + { + /// + /// Ignores all unmapped members by default - Use with caution! + /// + /// + /// + /// + /// + public static IMappingExpression IgnoreAllUnmapped(this IMappingExpression expression) + { + expression.ForAllMembers(opt => opt.Ignore()); + return expression; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs index 4f922d28cf..a7e794a400 100644 --- a/src/Umbraco.Core/Models/Media.cs +++ b/src/Umbraco.Core/Models/Media.cs @@ -118,17 +118,5 @@ namespace Umbraco.Core.Models //The Media Recycle Bin Id is -21 so we correct that here ParentId = parentId == -20 ? -21 : parentId; } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs new file mode 100644 index 0000000000..1f2e1b62b2 --- /dev/null +++ b/src/Umbraco.Core/Models/MediaExtensions.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Core.Models +{ + internal static class MediaExtensions + { + /// + /// Hack: we need to put this in a real place, this is currently just used to render the urls for a media item in the back office + /// + /// + public static string GetUrl(this IMedia media, string propertyAlias, ILogger logger) + { + var propertyType = media.PropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyAlias)); + if (propertyType != null) + { + var val = media.Properties[propertyType]; + if (val != null) + { + var jsonString = val.Value as string; + if (jsonString != null) + { + if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) + { + if (jsonString.DetectIsJson()) + { + try + { + var json = JsonConvert.DeserializeObject(jsonString); + if (json["src"] != null) + { + return json["src"].Value(); + } + } + catch (Exception ex) + { + logger.Error("Could not parse the string " + jsonString + " to a json object", ex); + return string.Empty; + } + } + else + { + return jsonString; + } + } + else if (propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) + { + return jsonString; + } + //hrm, without knowing what it is, just adding a string here might not be very nice + } + } + } + return string.Empty; + } + + /// + /// Hack: we need to put this in a real place, this is currently just used to render the urls for a media item in the back office + /// + /// + public static string[] GetUrls(this IMedia media, IContentSection contentSection, ILogger logger) + { + var links = new List(); + var autoFillProperties = contentSection.ImageAutoFillProperties.ToArray(); + if (autoFillProperties.Any()) + { + links.AddRange( + autoFillProperties + .Select(field => media.GetUrl(field.Alias, logger)) + .Where(link => link.IsNullOrWhiteSpace() == false)); + } + return links.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MediaType.cs b/src/Umbraco.Core/Models/MediaType.cs index 052e231136..8596cce910 100644 --- a/src/Umbraco.Core/Models/MediaType.cs +++ b/src/Umbraco.Core/Models/MediaType.cs @@ -40,24 +40,28 @@ namespace Umbraco.Core.Models } /// - /// Method to call when Entity is being saved + /// Creates a deep clone of the current entity with its identity/alias and it's property identities reset /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() + /// + public IMediaType DeepCloneWithResetIdentities(string alias) { - base.AddingEntity(); + var clone = (MediaType)DeepClone(); + clone.Alias = alias; + clone.Key = Guid.Empty; + foreach (var propertyGroup in clone.PropertyGroups) + { + propertyGroup.ResetIdentity(); + propertyGroup.ResetDirtyProperties(false); + } + foreach (var propertyType in clone.PropertyTypes) + { + propertyType.ResetIdentity(); + propertyType.ResetDirtyProperties(false); + } - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - - /// - /// Method to call when Entity is being updated - /// - /// Modified Date is set and a new Version guid is set - internal override void UpdatingEntity() - { - base.UpdatingEntity(); + clone.ResetIdentity(); + clone.ResetDirtyProperties(false); + return clone; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index d416c846ef..70c21e4307 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -280,7 +280,7 @@ namespace Umbraco.Core.Models //This is the default value if the prop is not found true); if (a.Success == false) return a.Result; - + if (Properties[Constants.Conventions.Member.IsApproved].Value == null) return true; var tryConvert = Properties[Constants.Conventions.Member.IsApproved].Value.TryConvertTo(); if (tryConvert.Success) { @@ -313,7 +313,7 @@ namespace Umbraco.Core.Models { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsLockedOut, "IsLockedOut", false); if (a.Success == false) return a.Result; - + if (Properties[Constants.Conventions.Member.IsLockedOut].Value == null) return false; var tryConvert = Properties[Constants.Conventions.Member.IsLockedOut].Value.TryConvertTo(); if (tryConvert.Success) { @@ -346,7 +346,7 @@ namespace Umbraco.Core.Models { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLoginDate, "LastLoginDate", default(DateTime)); if (a.Success == false) return a.Result; - + if (Properties[Constants.Conventions.Member.LastLoginDate].Value == null) return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLoginDate].Value.TryConvertTo(); if (tryConvert.Success) { @@ -379,7 +379,7 @@ namespace Umbraco.Core.Models { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastPasswordChangeDate, "LastPasswordChangeDate", default(DateTime)); if (a.Success == false) return a.Result; - + if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].Value == null) return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastPasswordChangeDate].Value.TryConvertTo(); if (tryConvert.Success) { @@ -412,7 +412,7 @@ namespace Umbraco.Core.Models { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLockoutDate, "LastLockoutDate", default(DateTime)); if (a.Success == false) return a.Result; - + if (Properties[Constants.Conventions.Member.LastLockoutDate].Value == null) return default(DateTime); var tryConvert = Properties[Constants.Conventions.Member.LastLockoutDate].Value.TryConvertTo(); if (tryConvert.Success) { @@ -446,7 +446,7 @@ namespace Umbraco.Core.Models { var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.FailedPasswordAttempts, "FailedPasswordAttempts", 0); if (a.Success == false) return a.Result; - + if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].Value == null) return default(int); var tryConvert = Properties[Constants.Conventions.Member.FailedPasswordAttempts].Value.TryConvertTo(); if (tryConvert.Success) { @@ -509,11 +509,8 @@ namespace Umbraco.Core.Models { base.AddingEntity(); - if (Key == Guid.Empty) - { - Key = Guid.NewGuid(); + if (ProviderUserKey == null) ProviderUserKey = Key; - } } /// diff --git a/src/Umbraco.Core/Models/MemberGroup.cs b/src/Umbraco.Core/Models/MemberGroup.cs index e52448a11d..ff7e05be9e 100644 --- a/src/Umbraco.Core/Models/MemberGroup.cs +++ b/src/Umbraco.Core/Models/MemberGroup.cs @@ -60,17 +60,5 @@ namespace Umbraco.Core.Models } public IDictionary AdditionalData { get; private set; } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs index 74a879be81..9000a33b54 100644 --- a/src/Umbraco.Core/Models/MemberType.cs +++ b/src/Umbraco.Core/Models/MemberType.cs @@ -131,26 +131,5 @@ namespace Umbraco.Core.Models MemberTypePropertyTypes.Add(propertyTypeAlias, tuple); } } - - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - - /// - /// Method to call when Entity is being updated - /// - /// Modified Date is set and a new Version guid is set - internal override void UpdatingEntity() - { - base.UpdatingEntity(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index d7c2eb92a8..f3fbefda4f 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -124,10 +124,54 @@ namespace Umbraco.Core.Models bool typeValidation = _propertyType.IsPropertyTypeValid(value); if (typeValidation == false) - throw new Exception( - string.Format( - "Type validation failed. The value type: '{0}' does not match the DataType in PropertyType with alias: '{1}'", - value == null ? "null" : value.GetType().Name, Alias)); + { + // Normally we'll throw an exception here. However if the property is of a type that can have it's data field (dataInt, dataVarchar etc.) + // changed, we might have a value of the now "wrong" type. As of May 2016 Label is the only built-in property editor that supports this. + // In that case we should try to parse the value and return null if that's not possible rather than throwing an exception. + if (value != null && _propertyType.CanHaveDataValueTypeChanged()) + { + var stringValue = value.ToString(); + switch (_propertyType.DataTypeDatabaseType) + { + case DataTypeDatabaseType.Nvarchar: + case DataTypeDatabaseType.Ntext: + value = stringValue; + break; + case DataTypeDatabaseType.Integer: + int integerValue; + if (int.TryParse(stringValue, out integerValue) == false) + { + // Edge case, but if changed from decimal --> integer, the above tryparse will fail. So we'll try going + // via decimal too to return the integer value rather than zero. + decimal decimalForIntegerValue; + if (decimal.TryParse(stringValue, out decimalForIntegerValue)) + { + integerValue = (int)decimalForIntegerValue; + } + } + + value = integerValue; + break; + case DataTypeDatabaseType.Decimal: + decimal decimalValue; + decimal.TryParse(stringValue, out decimalValue); + value = decimalValue; + break; + case DataTypeDatabaseType.Date: + DateTime dateValue; + DateTime.TryParse(stringValue, out dateValue); + value = dateValue; + break; + } + } + else + { + throw new Exception( + string.Format( + "Type validation failed. The value type: '{0}' does not match the DataType in PropertyType with alias: '{1}'", + value == null ? "null" : value.GetType().Name, Alias)); + } + } SetPropertyValueAndDetectChanges(o => { diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs index 7976a7eac6..de88012c0e 100644 --- a/src/Umbraco.Core/Models/PropertyGroup.cs +++ b/src/Umbraco.Core/Models/PropertyGroup.cs @@ -17,7 +17,6 @@ namespace Umbraco.Core.Models public class PropertyGroup : Entity, IEquatable { private string _name; - private Lazy _parentId; private int _sortOrder; private PropertyTypeCollection _propertyTypes; @@ -31,7 +30,6 @@ namespace Umbraco.Core.Models } private static readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); - private static readonly PropertyInfo ParentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ParentId); private static readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder); private readonly static PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes); void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -56,29 +54,6 @@ namespace Umbraco.Core.Models } } - /// - /// Gets or sets the Id of the Parent PropertyGroup. - /// - /// - /// A Parent PropertyGroup corresponds to an inherited PropertyGroup from a composition. - /// If a PropertyType is inserted into an inherited group then a new group will be created with an Id reference to the parent. - /// - [DataMember] - public int? ParentId - { - get - { - if (_parentId == null) - return default(int?); - return _parentId.Value; - } - set - { - _parentId = new Lazy(() => value); - OnPropertyChanged(ParentIdSelector); - } - } - /// /// Gets or sets the Sort Order of the Group /// @@ -117,15 +92,6 @@ namespace Umbraco.Core.Models } } - /// - /// Sets the ParentId from the lazy integer id - /// - /// Id of the Parent - internal void SetLazyParentId(Lazy id) - { - _parentId = id; - } - public bool Equals(PropertyGroup other) { if (base.Equals(other)) return true; diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index e9d4451314..846b907197 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; using System.Text.RegularExpressions; @@ -9,6 +10,7 @@ using Umbraco.Core.Strings; namespace Umbraco.Core.Models { + /// /// Defines the type of a object /// @@ -32,6 +34,8 @@ namespace Umbraco.Core.Models public PropertyType(IDataTypeDefinition dataTypeDefinition) { + if (dataTypeDefinition == null) throw new ArgumentNullException("dataTypeDefinition"); + if(dataTypeDefinition.HasIdentity) _dataTypeDefinitionId = dataTypeDefinition.Id; @@ -218,8 +222,9 @@ namespace Umbraco.Core.Models } /// - /// Gets or Sets the PropertyGroup's Id for which this PropertyType belongs + /// Gets or sets the identifier of the PropertyGroup this PropertyType belongs to. /// + /// For generic properties, the value is null. [DataMember] internal Lazy PropertyGroupId { @@ -401,6 +406,9 @@ namespace Umbraco.Core.Models if (DataTypeDatabaseType == DataTypeDatabaseType.Integer && type == typeof(int)) return true; + if (DataTypeDatabaseType == DataTypeDatabaseType.Decimal && type == typeof(decimal)) + return true; + if (DataTypeDatabaseType == DataTypeDatabaseType.Date && type == typeof(DateTime)) return true; @@ -418,6 +426,19 @@ namespace Umbraco.Core.Models return false; } + /// + /// Checks the underlying property editor prevalues to see if the one that allows changing of the database field + /// to which data is saved (dataInt, dataVarchar etc.) is included. If so that means the field could be changed when the data + /// type is saved. + /// + /// + internal bool CanHaveDataValueTypeChanged() + { + var propertyEditor = PropertyEditorResolver.Current.GetByAlias(_propertyEditorAlias); + return propertyEditor.PreValueEditor.Fields + .SingleOrDefault(x => x.Key == Constants.PropertyEditors.PreValueKeys.DataValueType) != null; + } + /// /// Validates the Value from a Property according to the validation settings /// diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 27d1cd2121..9e12d6ab57 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -108,18 +108,6 @@ namespace Umbraco.Core.Models } } - /// - /// Method to call on entity saved when first added - /// - internal override void AddingEntity() - { - if (Key == default(Guid)) - { - Key = Guid.NewGuid(); - } - base.AddingEntity(); - } - [DataMember] public int LoginNodeId { diff --git a/src/Umbraco.Core/Models/PublicAccessRule.cs b/src/Umbraco.Core/Models/PublicAccessRule.cs index 484652e8bd..c785d028d0 100644 --- a/src/Umbraco.Core/Models/PublicAccessRule.cs +++ b/src/Umbraco.Core/Models/PublicAccessRule.cs @@ -28,18 +28,6 @@ namespace Umbraco.Core.Models public Guid AccessEntryId { get; internal set; } - /// - /// Method to call on entity saved when first added - /// - internal override void AddingEntity() - { - if (Key == default(Guid)) - { - Key = Guid.NewGuid(); - } - base.AddingEntity(); - } - public string RuleValue { get { return _ruleValue; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 5f30c08ce7..d05960b08f 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Web.Caching; -using System.Web.UI; using Umbraco.Core.Cache; namespace Umbraco.Core.Models.PublishedContent @@ -16,6 +13,7 @@ namespace Umbraco.Core.Models.PublishedContent public class PublishedContentType { private readonly PublishedPropertyType[] _propertyTypes; + private readonly HashSet _compositionAliases; // fast alias-to-index xref containing both the raw alias and its lowercase version private readonly Dictionary _indexes = new Dictionary(); @@ -27,6 +25,7 @@ namespace Umbraco.Core.Models.PublishedContent { Id = contentType.Id; Alias = contentType.Alias; + _compositionAliases = new HashSet(contentType.CompositionAliases(), StringComparer.InvariantCultureIgnoreCase); _propertyTypes = contentType.CompositionPropertyTypes .Select(x => new PublishedPropertyType(this, x)) .ToArray(); @@ -34,10 +33,11 @@ namespace Umbraco.Core.Models.PublishedContent } // internal so it can be used for unit tests - internal PublishedContentType(int id, string alias, IEnumerable propertyTypes) + internal PublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) { Id = id; Alias = alias; + _compositionAliases = new HashSet(compositionAliases, StringComparer.InvariantCultureIgnoreCase); _propertyTypes = propertyTypes.ToArray(); foreach (var propertyType in _propertyTypes) propertyType.ContentType = this; @@ -45,8 +45,8 @@ namespace Umbraco.Core.Models.PublishedContent } // create detached content type - ie does not match anything in the DB - internal PublishedContentType(string alias, IEnumerable propertyTypes) - : this (0, alias, propertyTypes) + internal PublishedContentType(string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) + : this(0, alias, compositionAliases, propertyTypes) { } private void InitializeIndexes() @@ -63,6 +63,7 @@ namespace Umbraco.Core.Models.PublishedContent public int Id { get; private set; } public string Alias { get; private set; } + public HashSet CompositionAliases { get { return _compositionAliases; } } #endregion @@ -113,10 +114,25 @@ namespace Umbraco.Core.Models.PublishedContent internal static void ClearContentType(int id) { Logging.LogHelper.Debug("Clear content type w/id {0}.", () => id); - // requires a predicate because the key does not contain the ID - // faster than key strings comparisons anyway + + // we don't support "get all" at the moment - so, cheating + var all = ApplicationContext.Current.ApplicationCache.StaticCache.GetCacheItemsByKeySearch("PublishedContentType_").ToArray(); + + // the one we want to clear + var clr = all.FirstOrDefault(x => x.Id == id); + if (clr == null) return; + + // those that have that one in their composition aliases + // note: CompositionAliases contains all recursive aliases + var oth = all.Where(x => x.CompositionAliases.InvariantContains(clr.Alias)).Select(x => x.Id); + + // merge ids + var ids = oth.Concat(new[] { clr.Id }).ToArray(); + + // clear them all at once + // we don't support "clear many at once" at the moment - so, cheating ApplicationContext.Current.ApplicationCache.StaticCache.ClearCacheObjectTypes( - (key, value) => value.Id == id); + (key, value) => ids.Contains(value.Id)); } internal static void ClearDataType(int id) diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index f4b1597a7d..1875ae03b3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -164,6 +164,9 @@ namespace Umbraco.Core.Models.PublishedContent private void InitializeConverters() { + //TODO: Look at optimizing this method, it gets run for every property type for the document being rendered at startup, + // every precious second counts! + var converters = PropertyValueConvertersResolver.Current.Converters.ToArray(); var defaultConvertersWithAttributes = PropertyValueConvertersResolver.Current.DefaultConverters; @@ -230,13 +233,13 @@ namespace Umbraco.Core.Models.PublishedContent { _sourceCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.Source); _objectCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.Object); - _objectCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.XPath); + _xpathCacheLevel = converterMeta.GetPropertyCacheLevel(this, PropertyCacheValue.XPath); } else { _sourceCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Source); _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.Object); - _objectCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); + _xpathCacheLevel = GetCacheLevel(_converter, PropertyCacheValue.XPath); } if (_objectCacheLevel < _sourceCacheLevel) _objectCacheLevel = _sourceCacheLevel; if (_xpathCacheLevel < _sourceCacheLevel) _xpathCacheLevel = _sourceCacheLevel; diff --git a/src/Umbraco.Core/Models/Rdbms/DocumentTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/ContentTypeTemplateDto.cs similarity index 93% rename from src/Umbraco.Core/Models/Rdbms/DocumentTypeDto.cs rename to src/Umbraco.Core/Models/Rdbms/ContentTypeTemplateDto.cs index e3fdfb9810..88ef02ea90 100644 --- a/src/Umbraco.Core/Models/Rdbms/DocumentTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/ContentTypeTemplateDto.cs @@ -1,28 +1,28 @@ -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseAnnotations; - -namespace Umbraco.Core.Models.Rdbms -{ - [TableName("cmsDocumentType")] - [PrimaryKey("contentTypeNodeId", autoIncrement = false)] - [ExplicitColumns] - internal class DocumentTypeDto - { - [Column("contentTypeNodeId")] - [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] - [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] - [ForeignKey(typeof(NodeDto))] - public int ContentTypeNodeId { get; set; } - - [Column("templateNodeId")] - [ForeignKey(typeof(TemplateDto), Column = "nodeId")] - public int TemplateNodeId { get; set; } - - [Column("IsDefault")] - [Constraint(Default = "0")] - public bool IsDefault { get; set; } - - [ResultColumn] - public ContentTypeDto ContentTypeDto { get; set; } - } +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("cmsDocumentType")] + [PrimaryKey("contentTypeNodeId", autoIncrement = false)] + [ExplicitColumns] + internal class ContentTypeTemplateDto + { + [Column("contentTypeNodeId")] + [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_cmsDocumentType", OnColumns = "contentTypeNodeId, templateNodeId")] + [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] + [ForeignKey(typeof(NodeDto))] + public int ContentTypeNodeId { get; set; } + + [Column("templateNodeId")] + [ForeignKey(typeof(TemplateDto), Column = "nodeId")] + public int TemplateNodeId { get; set; } + + [Column("IsDefault")] + [Constraint(Default = "0")] + public bool IsDefault { get; set; } + + [ResultColumn] + public ContentTypeDto ContentTypeDto { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/MigrationDto.cs b/src/Umbraco.Core/Models/Rdbms/MigrationDto.cs index 5ab339fe01..1a76895e90 100644 --- a/src/Umbraco.Core/Models/Rdbms/MigrationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MigrationDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models.Rdbms internal class MigrationDto { [Column("id")] - [PrimaryKeyColumn(AutoIncrement = true)] + [PrimaryKeyColumn(AutoIncrement = true, IdentitySeed = 100)] public int Id { get; set; } [Column("name")] diff --git a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs index 7003c58e77..c5fac092df 100644 --- a/src/Umbraco.Core/Models/Rdbms/NodeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/NodeDto.cs @@ -10,12 +10,6 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class NodeDto { - public NodeDto() - { - //By default, always generate a new guid - UniqueId = Guid.NewGuid(); - } - public const int NodeIdSeed = 1050; [Column("id")] diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs index 4e59f0275b..63e3104d12 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs @@ -33,6 +33,10 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] public int? Integer { get; set; } + [Column("dataDecimal")] + [NullSetting(NullSetting = NullSettings.Null)] + public decimal? Decimal { get; set; } + [Column("dataDate")] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? Date { get; set; } @@ -55,22 +59,27 @@ namespace Umbraco.Core.Models.Rdbms { get { - if(Integer.HasValue) + if (Integer.HasValue) { return Integer.Value; } + + if (Decimal.HasValue) + { + return Decimal.Value; + } - if(Date.HasValue) + if (Date.HasValue) { return Date.Value; } - if(string.IsNullOrEmpty(VarChar) == false) + if (string.IsNullOrEmpty(VarChar) == false) { return VarChar; } - if(string.IsNullOrEmpty(Text) == false) + if (string.IsNullOrEmpty(Text) == false) { return Text; } diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs index 74a6d34289..2be4b24157 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyTypeDto.cs @@ -10,12 +10,6 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class PropertyTypeDto { - public PropertyTypeDto() - { - //by default always create a new guid - UniqueId = Guid.NewGuid(); - } - [Column("id")] [PrimaryKeyColumn(IdentitySeed = 50)] public int Id { get; set; } diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs index 51cb9872fe..68de420a97 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupDto.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; namespace Umbraco.Core.Models.Rdbms { @@ -13,12 +15,6 @@ namespace Umbraco.Core.Models.Rdbms [PrimaryKeyColumn(IdentitySeed = 12)] public int Id { get; set; } - [Column("parentGroupId")] - [NullSetting(NullSetting = NullSettings.Null)] - //[Constraint(Default = "NULL")] - [ForeignKey(typeof(PropertyTypeGroupDto))] - public int? ParentGroupId { get; set; } - [Column("contenttypeNodeId")] [ForeignKey(typeof(ContentTypeDto), Column = "nodeId")] public int ContentTypeNodeId { get; set; } @@ -31,5 +27,11 @@ namespace Umbraco.Core.Models.Rdbms [ResultColumn] public List PropertyTypeDtos { get; set; } + + [Column("uniqueID")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Constraint(Default = SystemMethods.NewGuid)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")] + public Guid UniqueId { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupReadOnlyDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupReadOnlyDto.cs index 8dcc4af29c..57c973fb64 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupReadOnlyDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyTypeGroupReadOnlyDto.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Persistence; +using System; +using Umbraco.Core.Persistence; namespace Umbraco.Core.Models.Rdbms { @@ -10,9 +11,6 @@ namespace Umbraco.Core.Models.Rdbms [Column("PropertyTypeGroupId")] public int? Id { get; set; } - [Column("parentGroupId")] - public int? ParentGroupId { get; set; } - [Column("PropertyGroupName")] public string Text { get; set; } @@ -21,5 +19,8 @@ namespace Umbraco.Core.Models.Rdbms [Column("contenttypeNodeId")] public int ContentTypeNodeId { get; set; } + + [Column("PropertyGroupUniqueID")] + public Guid UniqueId { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyTypeReadOnlyDto.cs index 2f448860b0..d85baa2884 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyTypeReadOnlyDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyTypeReadOnlyDto.cs @@ -51,5 +51,8 @@ namespace Umbraco.Core.Models.Rdbms [Column("dbType")] public string DbType { get; set; } + + [Column("UniqueID")] + public Guid UniqueId { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs new file mode 100644 index 0000000000..06d904ee88 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployChecksumDto.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoDeployChecksum")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class UmbracoDeployChecksumDto + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoDeployChecksum")] + public int Id { get; set; } + + [Column("entityType")] + [Length(32)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoDeployChecksum", ForColumns = "entityType,entityGuid,entityPath")] + public string EntityType { get; set; } + + [Column("entityGuid")] + [NullSetting(NullSetting = NullSettings.Null)] + public Guid EntityGuid { get; set; } + + [Column("entityPath")] + [Length(256)] + [NullSetting(NullSetting = NullSettings.Null)] + public string EntityPath { get; set; } + + [Column("localChecksum")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(32)] + public string LocalChecksum { get; set; } + + [Column("compositeChecksum")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(32)] + public string CompositeChecksum { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs new file mode 100644 index 0000000000..765a32c929 --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/UmbracoDeployDependencyDto.cs @@ -0,0 +1,26 @@ +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoDeployDependency")] + [ExplicitColumns] + internal class UmbracoDeployDependencyDto + { + [Column("sourceId")] + [PrimaryKeyColumn(AutoIncrement = false, Clustered = true, Name = "PK_umbracoDeployDependency", OnColumns = "sourceId, targetId")] + [ForeignKey(typeof(UmbracoDeployChecksumDto), Name = "FK_umbracoDeployDependency_umbracoDeployChecksum_id1")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int SourceId { get; set; } + + [Column("targetId")] + [ForeignKey(typeof(UmbracoDeployChecksumDto), Name = "FK_umbracoDeployDependency_umbracoDeployChecksum_id2")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int TargetId { get; set; } + + [Column("mode")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public int Mode { get; set; } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index 4aca88f286..1c8b44b674 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -118,18 +118,6 @@ namespace Umbraco.Core.Models return ApplicationContext.Current.Services.FileService.DetermineTemplateRenderingEngine(this); } - /// - /// Method to call when Entity is being saved - /// - /// Created date is set and a Unique key is assigned - internal override void AddingEntity() - { - base.AddingEntity(); - - if (Key == Guid.Empty) - Key = Guid.NewGuid(); - } - public void SetMasterTemplate(ITemplate masterTemplate) { if (masterTemplate == null) diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 4975ad3bd2..70ef056068 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Models /// /// Implementation of the for internal use. /// - internal class UmbracoEntity : Entity, IUmbracoEntity + public class UmbracoEntity : Entity, IUmbracoEntity { private int _creatorId; private int _level; diff --git a/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs b/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs index edcc25ade8..987be2a276 100644 --- a/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoEntityExtensions.cs @@ -11,6 +11,20 @@ namespace Umbraco.Core.Models internal static class UmbracoEntityExtensions { + public static bool HasChildren(this IUmbracoEntity entity) + { + if (entity.AdditionalData.ContainsKey("HasChildren")) + { + var convert = entity.AdditionalData["HasChildren"].TryConvertTo(); + if (convert) + { + return convert.Result; + } + } + return false; + } + + public static object GetAdditionalDataValueIgnoreCase(this IUmbracoEntity entity, string key, object defaultVal) { if (entity.AdditionalData.ContainsKeyIgnoreCase(key) == false) return defaultVal; diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 37ad181682..2be2010301 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -15,49 +15,49 @@ namespace Umbraco.Core.Models /// /// Content Item Type /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.ContentItemType)] + [UmbracoObjectType(Constants.ObjectTypes.ContentItemType)] [FriendlyName("Content Item Type")] ContentItemType, /// /// Root /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.SystemRoot)] + [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] [FriendlyName("Root")] ROOT, /// /// Document /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.Document, typeof(IContent))] + [UmbracoObjectType(Constants.ObjectTypes.Document, typeof(IContent))] [FriendlyName("Document")] Document, /// /// Media /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.Media, typeof(IMedia))] + [UmbracoObjectType(Constants.ObjectTypes.Media, typeof(IMedia))] [FriendlyName("Media")] Media, /// /// Member Type /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.MemberType, typeof(IMemberType))] + [UmbracoObjectType(Constants.ObjectTypes.MemberType, typeof(IMemberType))] [FriendlyName("Member Type")] MemberType, /// /// Template /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.Template, typeof(ITemplate))] + [UmbracoObjectType(Constants.ObjectTypes.Template, typeof(ITemplate))] [FriendlyName("Template")] Template, /// /// Member Group /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.MemberGroup)] + [UmbracoObjectType(Constants.ObjectTypes.MemberGroup)] [FriendlyName("Member Group")] MemberGroup, @@ -65,50 +65,73 @@ namespace Umbraco.Core.Models /// /// Content Item /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.ContentItem)] + [UmbracoObjectType(Constants.ObjectTypes.ContentItem)] [FriendlyName("Content Item")] ContentItem, /// /// "Media Type /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.MediaType, typeof(IMediaType))] + [UmbracoObjectType(Constants.ObjectTypes.MediaType, typeof(IMediaType))] [FriendlyName("Media Type")] MediaType, /// /// Document Type /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.DocumentType, typeof(IContentType))] + [UmbracoObjectType(Constants.ObjectTypes.DocumentType, typeof(IContentType))] [FriendlyName("Document Type")] DocumentType, /// /// Recycle Bin /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.ContentRecycleBin)] + [UmbracoObjectType(Constants.ObjectTypes.ContentRecycleBin)] [FriendlyName("Recycle Bin")] RecycleBin, /// /// Stylesheet /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.Stylesheet)] + [UmbracoObjectType(Constants.ObjectTypes.Stylesheet)] [FriendlyName("Stylesheet")] Stylesheet, /// /// Member /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.Member, typeof(IMember))] + [UmbracoObjectType(Constants.ObjectTypes.Member, typeof(IMember))] [FriendlyName("Member")] Member, /// /// Data Type /// - [UmbracoObjectTypeAttribute(Constants.ObjectTypes.DataType, typeof(IDataTypeDefinition))] + [UmbracoObjectType(Constants.ObjectTypes.DataType, typeof(IDataTypeDefinition))] [FriendlyName("Data Type")] - DataType + DataType, + + /// + /// Document type container + /// + [UmbracoObjectType(Constants.ObjectTypes.DocumentTypeContainer)] + [FriendlyName("Document Type Container")] + DocumentTypeContainer, + + /// + /// Media type container + /// + [UmbracoObjectType(Constants.ObjectTypes.MediaTypeContainer)] + [FriendlyName("Media Type Container")] + MediaTypeContainer, + + /// + /// Media type container + /// + [UmbracoObjectType(Constants.ObjectTypes.DataTypeContainer)] + [FriendlyName("Data Type Container")] + DataTypeContainer + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index bd670f3836..5b9f63cf48 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using System.Threading; +using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 90d173b49a..a8a1e80eb7 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -1,15 +1,10 @@ using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; -using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using System.Security; using System.Xml; namespace Umbraco.Core @@ -51,7 +46,7 @@ namespace Umbraco.Core public static Attempt TryConvertTo(this object input) { var result = TryConvertTo(input, typeof(T)); - if (!result.Success) + if (result.Success == false) { //just try a straight up conversion try @@ -64,7 +59,7 @@ namespace Umbraco.Core return Attempt.Fail(e); } } - return !result.Success ? Attempt.Fail() : Attempt.Succeed((T)result.Result); + return result.Success == false ? Attempt.Fail() : Attempt.Succeed((T)result.Result); } /// @@ -117,7 +112,7 @@ namespace Umbraco.Core } // we've already dealed with nullables, so any other generic types need to fall through - if (!destinationType.IsGenericType) + if (destinationType.IsGenericType == false) { if (input is string) { @@ -335,11 +330,11 @@ namespace Umbraco.Core return null; // we can't decide... } - private readonly static char[] NumberDecimalSeparatorsToNormalize = new[] {'.', ','}; + private static readonly char[] NumberDecimalSeparatorsToNormalize = new[] {'.', ','}; private static string NormalizeNumberDecimalSeparator(string s) { - var normalized = System.Threading.Thread.CurrentThread.CurrentUICulture.NumberFormat.NumberDecimalSeparator[0]; + var normalized = System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator[0]; return s.ReplaceMany(NumberDecimalSeparatorsToNormalize, normalized); } @@ -442,7 +437,7 @@ namespace Umbraco.Core { var props = TypeDescriptor.GetProperties(o); var d = new Dictionary(); - foreach (var prop in props.Cast().Where(x => !ignoreProperties.Contains(x.Name))) + foreach (var prop in props.Cast().Where(x => ignoreProperties.Contains(x.Name) == false)) { var val = prop.GetValue(o); if (val != null) @@ -478,13 +473,13 @@ namespace Umbraco.Core var items = (from object enumItem in enumerable let value = GetEnumPropertyDebugString(enumItem, levels) where value != null select value).Take(10).ToList(); - return items.Count() > 0 + return items.Any() ? "{{ {0} }}".InvariantFormat(String.Join(", ", items)) : null; } var props = obj.GetType().GetProperties(); - if ((props.Count() == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) + if ((props.Length == 2) && props[0].Name == "Key" && props[1].Name == "Value" && levels > -2) { try { @@ -500,12 +495,12 @@ namespace Umbraco.Core if (levels > -1) { var items = - from propertyInfo in props + (from propertyInfo in props let value = GetPropertyDebugString(propertyInfo, obj, levels) where value != null - select "{0}={1}".InvariantFormat(propertyInfo.Name, value); + select "{0}={1}".InvariantFormat(propertyInfo.Name, value)).ToArray(); - return items.Count() > 0 + return items.Any() ? "[{0}]:{{ {1} }}".InvariantFormat(obj.GetType().Name, String.Join(", ", items)) : null; } diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index fdd2759d76..54c7d8d2c9 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -1,98 +1,144 @@ using System; +using System.Collections.Generic; using System.Globalization; +using System.Linq; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Factories { + // factory for + // IContentType (document types) + // IMediaType (media types) + // IMemberType (member types) + // internal class ContentTypeFactory { - private readonly Guid _nodeObjectType; + #region IContentType - public ContentTypeFactory(Guid nodeObjectType) + public IContentType BuildContentTypeEntity(ContentTypeDto dto) { - _nodeObjectType = nodeObjectType; - } + var contentType = new ContentType(dto.NodeDto.ParentId); + BuildCommonEntity(contentType, dto); - #region Implementation of IEntityFactory - - public IContentType BuildEntity(DocumentTypeDto dto) - { - var contentType = new ContentType(dto.ContentTypeDto.NodeDto.ParentId) - { - Id = dto.ContentTypeDto.NodeDto.NodeId, - Key = dto.ContentTypeDto.NodeDto.UniqueId, - Alias = dto.ContentTypeDto.Alias, - Name = dto.ContentTypeDto.NodeDto.Text, - Icon = dto.ContentTypeDto.Icon, - Thumbnail = dto.ContentTypeDto.Thumbnail, - SortOrder = dto.ContentTypeDto.NodeDto.SortOrder, - Description = dto.ContentTypeDto.Description, - CreateDate = dto.ContentTypeDto.NodeDto.CreateDate, - Path = dto.ContentTypeDto.NodeDto.Path, - Level = dto.ContentTypeDto.NodeDto.Level, - CreatorId = dto.ContentTypeDto.NodeDto.UserId.Value, - AllowedAsRoot = dto.ContentTypeDto.AllowAtRoot, - IsContainer = dto.ContentTypeDto.IsContainer, - Trashed = dto.ContentTypeDto.NodeDto.Trashed, - DefaultTemplateId = dto.TemplateNodeId - }; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 contentType.ResetDirtyProperties(false); - return contentType; - } - public DocumentTypeDto BuildDto(IContentType entity) - { - var documentTypeDto = new DocumentTypeDto - {ContentTypeDto = BuildContentTypeDto(entity), ContentTypeNodeId = entity.Id}; - - var contentType = entity as ContentType; - if(contentType != null) - { - documentTypeDto.TemplateNodeId = contentType.DefaultTemplateId; - documentTypeDto.IsDefault = true; - } - return documentTypeDto; + return contentType; } #endregion - private ContentTypeDto BuildContentTypeDto(IContentType entity) + #region IMediaType + + public IMediaType BuildMediaTypeEntity(ContentTypeDto dto) { + var contentType = new MediaType(dto.NodeDto.ParentId); + BuildCommonEntity(contentType, dto); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + contentType.ResetDirtyProperties(false); + + return contentType; + } + + #endregion + + #region IMemberType + + public IMemberType BuildMemberTypeEntity(ContentTypeDto dto) + { + throw new NotImplementedException(); + } + + public IEnumerable BuildMemberTypeDtos(IMemberType entity) + { + var memberType = entity as MemberType; + if (memberType == null || memberType.PropertyTypes.Any() == false) + return Enumerable.Empty(); + + var dtos = memberType.PropertyTypes.Select(x => new MemberTypeDto + { + NodeId = entity.Id, + PropertyTypeId = x.Id, + CanEdit = memberType.MemberCanEditProperty(x.Alias), + ViewOnProfile = memberType.MemberCanViewProperty(x.Alias) + }).ToList(); + return dtos; + } + + #endregion + + #region Common + + private static void BuildCommonEntity(ContentTypeBase entity, ContentTypeDto dto) + { + entity.Id = dto.NodeDto.NodeId; + entity.Key = dto.NodeDto.UniqueId; + entity.Alias = dto.Alias; + entity.Name = dto.NodeDto.Text; + entity.Icon = dto.Icon; + entity.Thumbnail = dto.Thumbnail; + entity.SortOrder = dto.NodeDto.SortOrder; + entity.Description = dto.Description; + entity.CreateDate = dto.NodeDto.CreateDate; + entity.Path = dto.NodeDto.Path; + entity.Level = dto.NodeDto.Level; + entity.CreatorId = dto.NodeDto.UserId.Value; + entity.AllowedAsRoot = dto.AllowAtRoot; + entity.IsContainer = dto.IsContainer; + entity.Trashed = dto.NodeDto.Trashed; + } + + public ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) + { + Guid nodeObjectType; + if (entity is IContentType) + nodeObjectType = Constants.ObjectTypes.DocumentTypeGuid; + else if (entity is IMediaType) + nodeObjectType = Constants.ObjectTypes.MediaTypeGuid; + else if (entity is IMemberType) + nodeObjectType = Constants.ObjectTypes.MemberTypeGuid; + else + throw new Exception("oops: invalid entity."); + var contentTypeDto = new ContentTypeDto - { - Alias = entity.Alias, - Description = entity.Description, - Icon = entity.Icon, - Thumbnail = entity.Thumbnail, - NodeId = entity.Id, - AllowAtRoot = entity.AllowedAsRoot, - IsContainer = entity.IsContainer, - NodeDto = BuildNodeDto(entity) - }; + { + Alias = entity.Alias, + Description = entity.Description, + Icon = entity.Icon, + Thumbnail = entity.Thumbnail, + NodeId = entity.Id, + AllowAtRoot = entity.AllowedAsRoot, + IsContainer = entity.IsContainer, + NodeDto = BuildNodeDto(entity, nodeObjectType) + }; return contentTypeDto; } - private NodeDto BuildNodeDto(IContentType entity) + private static NodeDto BuildNodeDto(IUmbracoEntity entity, Guid nodeObjectType) { var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeObjectType = _nodeObjectType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; + { + CreateDate = entity.CreateDate, + NodeId = entity.Id, + Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), + NodeObjectType = nodeObjectType, + ParentId = entity.ParentId, + Path = entity.Path, + SortOrder = entity.SortOrder, + Text = entity.Name, + Trashed = false, + UniqueId = entity.Key, + UserId = entity.CreatorId + }; return nodeDto; } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs b/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs index 14dca6b366..1b9d73bdd4 100644 --- a/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DictionaryItemFactory.cs @@ -42,7 +42,7 @@ namespace Umbraco.Core.Persistence.Factories { var text = new LanguageTextDto { - LanguageId = translation.Language.Id, + LanguageId = translation.LanguageId, UniqueId = translation.Key, Value = translation.Value }; diff --git a/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs b/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs index 8dca2494d0..65297b9529 100644 --- a/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/DictionaryTranslationFactory.cs @@ -7,20 +7,19 @@ namespace Umbraco.Core.Persistence.Factories internal class DictionaryTranslationFactory { private readonly Guid _uniqueId; - private ILanguage _language; - public DictionaryTranslationFactory(Guid uniqueId, ILanguage language) + public DictionaryTranslationFactory(Guid uniqueId) { _uniqueId = uniqueId; - _language = language; } #region Implementation of IEntityFactory public IDictionaryTranslation BuildEntity(LanguageTextDto dto) { - var item = new DictionaryTranslation(_language, dto.Value, _uniqueId) + var item = new DictionaryTranslation(dto.LanguageId, dto.Value, _uniqueId) {Id = dto.PrimaryKey}; + //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 item.ResetDirtyProperties(false); @@ -31,7 +30,7 @@ namespace Umbraco.Core.Persistence.Factories { var text = new LanguageTextDto { - LanguageId = entity.Language.Id, + LanguageId = entity.LanguageId, UniqueId = _uniqueId, Value = entity.Value }; diff --git a/src/Umbraco.Core/Persistence/Factories/MediaTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/MediaTypeFactory.cs deleted file mode 100644 index 98048cd3a7..0000000000 --- a/src/Umbraco.Core/Persistence/Factories/MediaTypeFactory.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Globalization; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Rdbms; - -namespace Umbraco.Core.Persistence.Factories -{ - internal class MediaTypeFactory - { - private readonly Guid _nodeObjectType; - - public MediaTypeFactory(Guid nodeObjectType) - { - _nodeObjectType = nodeObjectType; - } - - #region Implementation of IEntityFactory - - public IMediaType BuildEntity(ContentTypeDto dto) - { - var contentType = new MediaType(dto.NodeDto.ParentId) - { - Id = dto.NodeDto.NodeId, - Key = dto.NodeDto.UniqueId, - Alias = dto.Alias, - Name = dto.NodeDto.Text, - Icon = dto.Icon, - Thumbnail = dto.Thumbnail, - SortOrder = dto.NodeDto.SortOrder, - Description = dto.Description, - CreateDate = dto.NodeDto.CreateDate, - Path = dto.NodeDto.Path, - Level = dto.NodeDto.Level, - CreatorId = dto.NodeDto.UserId.Value, - AllowedAsRoot = dto.AllowAtRoot, - IsContainer = dto.IsContainer, - Trashed = dto.NodeDto.Trashed - }; - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - contentType.ResetDirtyProperties(false); - return contentType; - } - - public ContentTypeDto BuildDto(IMediaType entity) - { - var contentTypeDto = new ContentTypeDto - { - Alias = entity.Alias, - Description = entity.Description, - Icon = entity.Icon, - Thumbnail = entity.Thumbnail, - NodeId = entity.Id, - AllowAtRoot = entity.AllowedAsRoot, - IsContainer = entity.IsContainer, - NodeDto = BuildNodeDto(entity) - }; - return contentTypeDto; - } - - #endregion - - private NodeDto BuildNodeDto(IMediaType entity) - { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeObjectType = _nodeObjectType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return nodeDto; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeFactory.cs deleted file mode 100644 index 5879016fa6..0000000000 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeFactory.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Rdbms; - -namespace Umbraco.Core.Persistence.Factories -{ - internal class MemberTypeFactory - { - private readonly Guid _nodeObjectType; - - public MemberTypeFactory(Guid nodeObjectType) - { - _nodeObjectType = nodeObjectType; - } - - public IMemberType BuildEntity(ContentTypeDto dto) - { - throw new System.NotImplementedException(); - } - - public ContentTypeDto BuildDto(IMemberType entity) - { - var contentTypeDto = new ContentTypeDto - { - Alias = entity.Alias, - Description = entity.Description, - Icon = entity.Icon, - Thumbnail = entity.Thumbnail, - NodeId = entity.Id, - AllowAtRoot = entity.AllowedAsRoot, - IsContainer = entity.IsContainer, - NodeDto = BuildNodeDto(entity) - }; - return contentTypeDto; - } - - public IEnumerable BuildMemberTypeDtos(IMemberType entity) - { - var memberType = entity as MemberType; - if (memberType == null || memberType.PropertyTypes.Any() == false) - return Enumerable.Empty(); - - var memberTypes = new List(); - foreach (var propertyType in memberType.PropertyTypes) - { - memberTypes.Add(new MemberTypeDto - { - NodeId = entity.Id, - PropertyTypeId = propertyType.Id, - CanEdit = memberType.MemberCanEditProperty(propertyType.Alias), - ViewOnProfile = memberType.MemberCanViewProperty(propertyType.Alias) - }); - } - - return memberTypes; - } - - private NodeDto BuildNodeDto(IMemberType entity) - { - var nodeDto = new NodeDto - { - CreateDate = entity.CreateDate, - NodeId = entity.Id, - Level = short.Parse(entity.Level.ToString(CultureInfo.InvariantCulture)), - NodeObjectType = _nodeObjectType, - ParentId = entity.ParentId, - Path = entity.Path, - SortOrder = entity.SortOrder, - Text = entity.Name, - Trashed = false, - UniqueId = entity.Key, - UserId = entity.CreatorId - }; - return nodeDto; - } - - private int DeterminePropertyTypeId(int initialId, string alias, IEnumerable propertyTypes) - { - if (initialId == 0 || initialId == default(int)) - { - var propertyType = propertyTypes.SingleOrDefault(x => x.Alias.Equals(alias)); - if (propertyType == null) - return default(int); - - return propertyType.Id; - } - - return initialId; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index 38ef4542f4..468ea1b8bb 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -51,33 +51,31 @@ namespace Umbraco.Core.Persistence.Factories memberType.MemberTypePropertyTypes.Add(standardPropertyType.Key, new MemberTypePropertyProfileAccess(false, false)); } - memberType.PropertyTypes = propertyTypes; + memberType.NoGroupPropertyTypes = propertyTypes; return memberType; } private PropertyGroupCollection GetPropertyTypeGroupCollection(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary standardProps) { - var propertyGroups = new PropertyGroupCollection(); - + // see PropertyGroupFactory, repeating code here... + + var propertyGroups = new PropertyGroupCollection(); foreach (var groupDto in dto.PropertyTypeGroups.Where(x => x.Id.HasValue)) { var group = new PropertyGroup(); - - //Only assign an Id if the PropertyGroup belongs to this ContentType + + // if the group is defined on the current member type, + // assign its identifier, else it will be zero if (groupDto.ContentTypeNodeId == memberType.Id) { + // note: no idea why Id is nullable here, but better check + if (groupDto.Id.HasValue == false) + throw new Exception("oops: groupDto.Id has no value."); group.Id = groupDto.Id.Value; - - if (groupDto.ParentGroupId.HasValue) - group.ParentId = groupDto.ParentGroupId.Value; - } - else - { - //If the PropertyGroup is inherited, we add a reference to the group as a Parent. - group.ParentId = groupDto.Id; } + group.Key = groupDto.UniqueId; group.Name = groupDto.Text; group.SortOrder = groupDto.SortOrder; group.PropertyTypes = new PropertyTypeCollection(); @@ -117,7 +115,8 @@ namespace Umbraco.Core.Persistence.Factories ValidationRegExp = typeDto.ValidationRegExp, PropertyGroupId = new Lazy(() => tempGroupDto.Id.Value), CreateDate = memberType.CreateDate, - UpdateDate = memberType.UpdateDate + UpdateDate = memberType.UpdateDate, + Key = typeDto.UniqueId }; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -167,9 +166,10 @@ namespace Umbraco.Core.Persistence.Factories Name = typeDto.Name, SortOrder = typeDto.SortOrder, ValidationRegExp = typeDto.ValidationRegExp, - PropertyGroupId = new Lazy(() => default(int)), + PropertyGroupId = null, CreateDate = dto.CreateDate, - UpdateDate = dto.CreateDate + UpdateDate = dto.CreateDate, + Key = typeDto.UniqueId }; propertyTypes.Add(propertyType); diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 4e3653bf9e..8d51b627ea 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -63,7 +63,9 @@ namespace Umbraco.Core.Persistence.Factories //Check if property has an Id and set it, so that it can be updated if it already exists if (property.HasIdentity) + { dto.Id = property.Id; + } if (property.DataTypeDatabaseType == DataTypeDatabaseType.Integer) { @@ -82,11 +84,21 @@ namespace Umbraco.Core.Persistence.Factories } } } + else if (property.DataTypeDatabaseType == DataTypeDatabaseType.Decimal && property.Value != null) + { + decimal val; + if (decimal.TryParse(property.Value.ToString(), out val)) + { + dto.Decimal = val; + } + } else if (property.DataTypeDatabaseType == DataTypeDatabaseType.Date && property.Value != null && string.IsNullOrWhiteSpace(property.Value.ToString()) == false) { DateTime date; - if(DateTime.TryParse(property.Value.ToString(), out date)) + if (DateTime.TryParse(property.Value.ToString(), out date)) + { dto.Date = date; + } } else if (property.DataTypeDatabaseType == DataTypeDatabaseType.Ntext && property.Value != null) { diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs index 443457b0d9..2dfb996bb3 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs @@ -8,21 +8,21 @@ namespace Umbraco.Core.Persistence.Factories { internal class PropertyGroupFactory { - private readonly int _id; + private readonly int _contentTypeId; private readonly DateTime _createDate; private readonly DateTime _updateDate; //a callback to create a property type which can be injected via a contructor private readonly Func _propertyTypeCtor; - public PropertyGroupFactory(int id) + public PropertyGroupFactory(int contentTypeId) { - _id = id; + _contentTypeId = contentTypeId; _propertyTypeCtor = (propertyEditorAlias, dbType, alias) => new PropertyType(propertyEditorAlias, dbType); } - public PropertyGroupFactory(int id, DateTime createDate, DateTime updateDate, Func propertyTypeCtor) + public PropertyGroupFactory(int contentTypeId, DateTime createDate, DateTime updateDate, Func propertyTypeCtor) { - _id = id; + _contentTypeId = contentTypeId; _createDate = createDate; _updateDate = updateDate; _propertyTypeCtor = propertyTypeCtor; @@ -30,29 +30,24 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory,IEnumerable> - public IEnumerable BuildEntity(IEnumerable dto) + public IEnumerable BuildEntity(IEnumerable groupDtos) { + // groupDtos contains all the groups, those that are defined on the current + // content type, and those that are inherited from composition content types var propertyGroups = new PropertyGroupCollection(); - foreach (var groupDto in dto) + foreach (var groupDto in groupDtos) { var group = new PropertyGroup(); - //Only assign an Id if the PropertyGroup belongs to this ContentType - if (groupDto.ContentTypeNodeId == _id) - { - group.Id = groupDto.Id; - if (groupDto.ParentGroupId.HasValue) - group.ParentId = groupDto.ParentGroupId.Value; - } - else - { - //If the PropertyGroup is inherited, we add a reference to the group as a Parent. - group.ParentId = groupDto.Id; - } + // if the group is defined on the current content type, + // assign its identifier, else it will be zero + if (groupDto.ContentTypeNodeId == _contentTypeId) + group.Id = groupDto.Id; group.Name = groupDto.Text; group.SortOrder = groupDto.SortOrder; group.PropertyTypes = new PropertyTypeCollection(); + group.Key = groupDto.UniqueId; //Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded var typeDtos = groupDto.PropertyTypeDtos.Where(x => x.Id > 0); @@ -101,14 +96,12 @@ namespace Umbraco.Core.Persistence.Factories { var dto = new PropertyTypeGroupDto { - ContentTypeNodeId = _id, + ContentTypeNodeId = _contentTypeId, SortOrder = propertyGroup.SortOrder, - Text = propertyGroup.Name + Text = propertyGroup.Name, + UniqueId = propertyGroup.Key }; - if (propertyGroup.ParentId.HasValue) - dto.ParentGroupId = propertyGroup.ParentId.Value; - if (propertyGroup.HasIdentity) dto.Id = propertyGroup.Id; @@ -122,18 +115,14 @@ namespace Umbraco.Core.Persistence.Factories var propertyTypeDto = new PropertyTypeDto { Alias = propertyType.Alias, - ContentTypeId = _id, + ContentTypeId = _contentTypeId, DataTypeId = propertyType.DataTypeDefinitionId, Description = propertyType.Description, Mandatory = propertyType.Mandatory, Name = propertyType.Name, SortOrder = propertyType.SortOrder, ValidationRegExp = propertyType.ValidationRegExp, - UniqueId = propertyType.HasIdentity - ? propertyType.Key == Guid.Empty - ? Guid.NewGuid() - : propertyType.Key - : Guid.NewGuid() + UniqueId = propertyType.Key }; if (tabId != default(int)) diff --git a/src/Umbraco.Core/Persistence/Mappers/PropertyGroupMapper.cs b/src/Umbraco.Core/Persistence/Mappers/PropertyGroupMapper.cs index d911db1967..655c40fc61 100644 --- a/src/Umbraco.Core/Persistence/Mappers/PropertyGroupMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/PropertyGroupMapper.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Mappers internal override void BuildMap() { CacheMap(src => src.Id, dto => dto.Id); - CacheMap(src => src.ParentId, dto => dto.ParentGroupId); + CacheMap(src => src.Key, dto => dto.UniqueId); CacheMap(src => src.SortOrder, dto => dto.SortOrder); CacheMap(src => src.Name, dto => dto.Text); } diff --git a/src/Umbraco.Core/Persistence/Mappers/TaskTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TaskTypeMapper.cs new file mode 100644 index 0000000000..4399fd323b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Mappers/TaskTypeMapper.cs @@ -0,0 +1,38 @@ +using System.Collections.Concurrent; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Mappers +{ + /// + /// Represents a to DTO mapper used to translate the properties of the public api + /// implementation to that of the database's DTO as sql: [tableName].[columnName]. + /// + [MapperFor(typeof(TaskType))] + public sealed class TaskTypeMapper : BaseMapper + { + private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); + + //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it + // otherwise that would fail because there is no public constructor. + public TaskTypeMapper() + { + BuildMap(); + } + + #region Overrides of BaseMapper + + internal override ConcurrentDictionary PropertyInfoCache + { + get { return PropertyInfoCacheInstance; } + } + + internal override void BuildMap() + { + CacheMap(src => src.Id, dto => dto.Id); + CacheMap(src => src.Alias, dto => dto.Alias); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs index 08a7de0777..0d5b485ffa 100644 --- a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs @@ -39,6 +39,7 @@ namespace Umbraco.Core.Persistence.Mappers { CacheMap(src => src.Id, dto => dto.NodeId); CacheMap(src => src.MasterTemplateId, dto => dto.ParentId); + CacheMap(src => src.Key, dto => dto.UniqueId); CacheMap(src => src.Alias, dto => dto.Alias); CacheMap(src => src.Content, dto => dto.Design); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index fbe7675836..8046de454b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -108,7 +108,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -21, Trashed = false, ParentId = -1, UserId = 0, Level = 0, Path = "-1,-21", SortOrder = 0, UniqueId = new Guid("BF7C7CBC-952F-4518-97A2-69E9C7B33842"), Text = "Recycle Bin", NodeObjectType = new Guid(Constants.ObjectTypes.MediaRecycleBin), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -92, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-92", SortOrder = 35, UniqueId = new Guid("f0bc4bfb-b499-40d6-ba86-058885a5178c"), Text = "Label", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -90, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-90", SortOrder = 34, UniqueId = new Guid("84c6b441-31df-4ffe-b67e-67d5bc3ae65a"), Text = "Upload", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -89, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-89", SortOrder = 33, UniqueId = new Guid("c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"), Text = "Textbox multiple", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -89, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-89", SortOrder = 33, UniqueId = new Guid("c6bac0dd-4ab9-45b1-8e30-e4b619ee5da3"), Text = "Textarea", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -88, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-88", SortOrder = 32, UniqueId = new Guid("0cc0eba1-9960-42c9-bf9b-60e150b429ae"), Text = "Textstring", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -87, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-87", SortOrder = 4, UniqueId = new Guid("ca90c950-0aff-4e72-b976-a30b1ac57dad"), Text = "Richtext editor", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -51, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-51", SortOrder = 2, UniqueId = new Guid("2e6d3631-066e-44b8-aec4-96f09099b2b5"), Text = "Numeric", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); @@ -117,8 +117,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -42, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-42", SortOrder = 2, UniqueId = new Guid("0b6a45e7-44ba-430d-9da5-4e46060b9e03"), Text = "Dropdown", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -41, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-41", SortOrder = 2, UniqueId = new Guid("5046194e-4237-453c-a547-15db3a07c4e1"), Text = "Date Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -40, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-40", SortOrder = 2, UniqueId = new Guid("bb5f57c9-ce2b-4bb9-b697-4caca783a805"), Text = "Radiobox", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -39, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-39", SortOrder = 2, UniqueId = new Guid("f38f0ac7-1d27-439c-9f3f-089cd8825a53"), Text = "Dropdown multiple", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -38, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-38", SortOrder = 2, UniqueId = new Guid("fd9f1447-6c61-4a7c-9595-5aa39147d318"), Text = "Folder Browser", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -39, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-39", SortOrder = 2, UniqueId = new Guid("f38f0ac7-1d27-439c-9f3f-089cd8825a53"), Text = "Dropdown multiple", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -37, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-37", SortOrder = 2, UniqueId = new Guid("0225af17-b302-49cb-9176-b9f35cab9c17"), Text = "Approved Color", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = -36, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-36", SortOrder = 2, UniqueId = new Guid("e4d66c0f-b935-4200-81f0-025f7256b89a"), Text = "Date Picker with time", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.System.DefaultContentListViewDataTypeId, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-95", SortOrder = 2, UniqueId = new Guid("C0808DD3-8133-4E4B-8CE8-E2BEA84A96A4"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Content", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); @@ -135,13 +134,16 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = new Guid(Constants.ObjectTypes.MemberType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1045, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1045", SortOrder = 2, UniqueId = new Guid("7E3962CC-CE20-4FFC-B661-5897A894BA7E"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - + //TODO: We're not creating these for 7.0 //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1039, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1039", SortOrder = 2, UniqueId = new Guid("06f349a9-c949-4b6a-8660-59c10451af42"), Text = "Ultimate Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1038, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1038", SortOrder = 2, UniqueId = new Guid("1251c96c-185c-4e9b-93f4-b48205573cbd"), Text = "Simple Editor", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - + //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1042, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1042", SortOrder = 2, UniqueId = new Guid("0a452bd5-83f9-4bc3-8403-1286e13fb77e"), Text = "Macro Container", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + + // all lock objects + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.System.ServersLock, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1," + Constants.System.ServersLock, SortOrder = 1, UniqueId = new Guid("0AF5E610-A310-4B6F-925F-E928D5416AF7"), Text = "LOCK: Servers", NodeObjectType = Constants.ObjectTypes.LockObjectGuid, CreateDate = DateTime.Now }); } private void CreateCmsContentTypeData() @@ -179,16 +181,16 @@ namespace Umbraco.Core.Persistence.Migrations.Initial private void CreateCmsPropertyTypeGroupData() { - _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1 }); - _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1 }); - _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 5, ContentTypeNodeId = 1031, Text = "Contents", SortOrder = 1 }); + _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Image) }); + _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.File) }); + _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 5, ContentTypeNodeId = 1031, Text = "Contents", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Contents) }); //membership property group - _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1 }); + _database.Insert("cmsPropertyTypeGroup", "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Membership) }); } private void CreateCmsPropertyTypeData() { - _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = -90, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); + _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); @@ -196,7 +198,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); - _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = -38, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); + _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.System.DefaultMediaListViewDataTypeId, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); //membership property types _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null }); _database.Insert("cmsPropertyType", "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = -92, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null }); @@ -231,16 +233,15 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 6, DataTypeId = -90, PropertyEditorAlias = Constants.PropertyEditors.UploadFieldAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 7, DataTypeId = -92, PropertyEditorAlias = Constants.PropertyEditors.NoEditAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 8, DataTypeId = -36, PropertyEditorAlias = Constants.PropertyEditors.DateTimeAlias, DbType = "Date" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 9, DataTypeId = -37, PropertyEditorAlias = Constants.PropertyEditors.ColorPickerAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 10, DataTypeId = -38, PropertyEditorAlias = Constants.PropertyEditors.FolderBrowserAlias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 9, DataTypeId = -37, PropertyEditorAlias = Constants.PropertyEditors.ColorPickerAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 11, DataTypeId = -39, PropertyEditorAlias = Constants.PropertyEditors.DropDownListMultipleAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 12, DataTypeId = -40, PropertyEditorAlias = Constants.PropertyEditors.RadioButtonListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 13, DataTypeId = -41, PropertyEditorAlias = Constants.PropertyEditors.DateAlias, DbType = "Date" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 14, DataTypeId = -42, PropertyEditorAlias = Constants.PropertyEditors.DropDownListAlias, DbType = "Integer" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 16, DataTypeId = 1034, PropertyEditorAlias = Constants.PropertyEditors.ContentPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 17, DataTypeId = 1035, PropertyEditorAlias = Constants.PropertyEditors.MediaPickerAlias, DbType = "Integer" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 18, DataTypeId = 1036, PropertyEditorAlias = Constants.PropertyEditors.MemberPickerAlias, DbType = "Integer" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 17, DataTypeId = 1035, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 21, DataTypeId = 1040, PropertyEditorAlias = Constants.PropertyEditors.RelatedLinksAlias, DbType = "Ntext" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 22, DataTypeId = 1041, PropertyEditorAlias = Constants.PropertyEditors.TagsAlias, DbType = "Ntext" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 24, DataTypeId = 1043, PropertyEditorAlias = Constants.PropertyEditors.ImageCropperAlias, DbType = "Ntext" }); @@ -253,7 +254,6 @@ namespace Umbraco.Core.Persistence.Migrations.Initial //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 19, DataTypeId = 1038, PropertyEditorAlias = Constants.PropertyEditors.MarkdownEditorAlias, DbType = "Ntext" }); //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 20, DataTypeId = 1039, PropertyEditorAlias = Constants.PropertyEditors.UltimatePickerAlias, DbType = "Ntext" }); //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 23, DataTypeId = 1042, PropertyEditorAlias = Constants.PropertyEditors.MacroContainerAlias, DbType = "Ntext" }); - } private void CreateCmsDataTypePreValuesData() @@ -269,6 +269,17 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -2, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "Name" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -3, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "asc" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -4, Alias = "includeProperties", SortOrder = 4, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "[{\"alias\":\"email\",\"isSystem\":1},{\"alias\":\"username\",\"isSystem\":1},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1}]" }); + + //layouts for the list view + var cardLayout = "{\"name\": \"Grid\",\"path\": \"views/propertyeditors/listview/layouts/grid/grid.html\", \"icon\": \"icon-thumbnails-small\", \"isSystem\": 1, \"selected\": true}"; + var listLayout = "{\"name\": \"List\",\"path\": \"views/propertyeditors/listview/layouts/list/list.html\",\"icon\": \"icon-list\", \"isSystem\": 1,\"selected\": true}"; + + //defaults for the media list + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -5, Alias = "pageSize", SortOrder = 1, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "100" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -6, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "VersionDate" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -7, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "desc" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -8, Alias = "layouts", SortOrder = 4, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "[" + cardLayout + "," + listLayout + "]" }); + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -9, Alias = "includeProperties", SortOrder = 5, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "[{\"alias\":\"sortOrder\",\"isSystem\":1, \"header\": \"Sort order\"},{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]" }); } private void CreateUmbracoRelationTypeData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 0ba269a3ba..0ae044d1ee 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -44,7 +44,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {4, typeof (ContentVersionDto)}, {5, typeof (DocumentDto)}, - {6, typeof (DocumentTypeDto)}, + {6, typeof (ContentTypeTemplateDto)}, {7, typeof (DataTypeDto)}, {8, typeof (DataTypePreValueDto)}, {9, typeof (DictionaryDto)}, @@ -82,7 +82,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {42, typeof (AccessRuleDto)}, {43, typeof(CacheInstructionDto)}, {44, typeof (ExternalLoginDto)}, - {45, typeof (MigrationDto)} + {45, typeof (MigrationDto)}, + {46, typeof (UmbracoDeployChecksumDto)}, + {47, typeof (UmbracoDeployDependencyDto)} }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index 04d6598d4b..9af6d46fbb 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -119,6 +119,12 @@ namespace Umbraco.Core.Persistence.Migrations.Initial return new Version(7, 2, 5); } + //if the error is for umbracoDeployChecksum it must be the previous version to 7.4 since that is when it is added + if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoDeployChecksum")))) + { + return new Version(7, 3, 4); + } + return UmbracoVersion.Current; } diff --git a/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs b/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs index 234cd67386..081865b9a1 100644 --- a/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs +++ b/src/Umbraco.Core/Persistence/Migrations/MigrationRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Text; @@ -27,18 +28,21 @@ namespace Umbraco.Core.Persistence.Migrations private readonly IMigration[] _migrations; [Obsolete("Use the ctor that specifies all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public MigrationRunner(Version currentVersion, Version targetVersion, string productName) : this(LoggerResolver.Current.Logger, currentVersion, targetVersion, productName) { } [Obsolete("Use the ctor that specifies all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public MigrationRunner(ILogger logger, Version currentVersion, Version targetVersion, string productName) : this(logger, currentVersion, targetVersion, productName, null) { } [Obsolete("Use the ctor that specifies all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public MigrationRunner(ILogger logger, Version currentVersion, Version targetVersion, string productName, params IMigration[] migrations) : this(ApplicationContext.Current.Services.MigrationEntryService, logger, new SemVersion(currentVersion), new SemVersion(targetVersion), productName, migrations) { @@ -92,7 +96,7 @@ namespace Umbraco.Core.Persistence.Migrations : OrderedDowngradeMigrations(foundMigrations).ToList(); - if (Migrating.IsRaisedEventCancelled(new MigrationEventArgs(migrations, _currentVersion, _targetVersion, true), this)) + if (Migrating.IsRaisedEventCancelled(new MigrationEventArgs(migrations, _currentVersion, _targetVersion, _productName, true), this)) { _logger.Warn("Migration was cancelled by an event"); return false; @@ -121,7 +125,7 @@ namespace Umbraco.Core.Persistence.Migrations throw; } - Migrated.RaiseEvent(new MigrationEventArgs(migrations, migrationContext, _currentVersion, _targetVersion, false), this); + Migrated.RaiseEvent(new MigrationEventArgs(migrations, migrationContext, _currentVersion, _targetVersion, _productName, false), this); return true; } @@ -289,7 +293,7 @@ namespace Umbraco.Core.Persistence.Migrations //NOTE: We CANNOT do this as part of the transaction!!! This is because when upgrading to 7.3, we cannot // create the migrations table and then add data to it in the same transaction without issuing things like GO // commands and since we need to support all Dbs, we need to just do this after the fact. - var exists = _migrationEntryService.FindEntry(GlobalSettings.UmbracoMigrationName, _targetVersion); + var exists = _migrationEntryService.FindEntry(_productName, _targetVersion); if (exists == null) { _migrationEntryService.CreateEntry(_productName, _targetVersion); @@ -308,4 +312,4 @@ namespace Umbraco.Core.Persistence.Migrations /// public static event TypedEventHandler Migrated; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs new file mode 100644 index 0000000000..4e8d3165fb --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionFourOneZero/AddPreviewXmlTable.cs @@ -0,0 +1,32 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourOneZero +{ + [Migration("4.1.0", 0, GlobalSettings.UmbracoMigrationName)] + public class AddPreviewXmlTable : MigrationBase + { + public AddPreviewXmlTable(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var tableName = "cmsPreviewXml"; + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains(tableName)) return; + + Create.Table(tableName) + .WithColumn("nodeId").AsInt32().NotNullable() + .WithColumn("versionId").AsGuid().NotNullable() + .WithColumn("timestamp").AsDateTime().NotNullable() + .WithColumn("xml").AsString(); + } + + public override void Down() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs index 830adfd7fb..749996ea36 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/UpdateRelatedLinksData.cs @@ -32,17 +32,19 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { if (database != null) { - var dtSql = new Sql().Select("nodeId").From().Where(dto => dto.PropertyEditorAlias == Constants.PropertyEditors.RelatedLinksAlias); + var dtSql = new Sql().Select("nodeId") + .From(SqlSyntax) + .Where(dto => dto.PropertyEditorAlias == Constants.PropertyEditors.RelatedLinksAlias); + var dataTypeIds = database.Fetch(dtSql); + if (dataTypeIds.Any() == false) return string.Empty; - if (!dataTypeIds.Any()) return string.Empty; + // need to use dynamic, as PropertyDataDto has new properties (eg decimal...) in further versions that don't exist yet + var propertyData = database.Fetch("SELECT * FROM cmsPropertyData" + + " WHERE propertyTypeId in (SELECT id from cmsPropertyType where dataTypeID IN (@dataTypeIds))", new { /*dataTypeIds =*/ dataTypeIds }); + if (propertyData.Any() == false) return string.Empty; - var propertyData = - database.Fetch( - "WHERE propertyTypeId in (SELECT id from cmsPropertyType where dataTypeID IN (@dataTypeIds))", new { dataTypeIds = dataTypeIds }); - if (!propertyData.Any()) return string.Empty; - - var nodesIdsWithProperty = propertyData.Select(x => x.NodeId).Distinct().ToArray(); + var nodesIdsWithProperty = propertyData.Select(x => (int) x.contentNodeId).Distinct().ToArray(); var cmsContentXmlEntries = new List(); //We're doing an "IN" query here but SQL server only supports 2100 query parameters so we're going to split on that @@ -54,30 +56,33 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven cmsContentXmlEntries.AddRange(database.Fetch("WHERE nodeId in (@nodeIds)", new { nodeIds = batch })); } - var propertyTypeIds = propertyData.Select(x => x.PropertyTypeId).Distinct(); + var propertyTypeIds = propertyData.Select(x => (int) x.propertytypeid).Distinct(); //NOTE: We are writing the full query because we've added a column to the PropertyTypeDto in later versions so one of the columns // won't exist yet var propertyTypes = database.Fetch("SELECT * FROM cmsPropertyType WHERE id in (@propertyTypeIds)", new { propertyTypeIds = propertyTypeIds }); - + foreach (var data in propertyData) { - if (string.IsNullOrEmpty(data.Text) == false) + if (string.IsNullOrEmpty(data.dataNtext) == false) { XmlDocument xml; //fetch the current data (that's in xml format) try { xml = new XmlDocument(); - xml.LoadXml(data.Text); + xml.LoadXml(data.dataNtext); } - catch (Exception ex) + catch (Exception ex) { - Logger.Error("The data stored for property id " + data.Id + " on document " + data.NodeId + - " is not valid XML, the data will be removed because it cannot be converted to the new format. The value was: " + data.Text, ex); + int dataId = data.id; + int dataNodeId = data.contentNodeId; + string dataText = data.dataNtext; + Logger.Error("The data stored for property id " + dataId + " on document " + dataNodeId + + " is not valid XML, the data will be removed because it cannot be converted to the new format. The value was: " + dataText, ex); - data.Text = ""; - database.Update(data); + data.dataNtext = ""; + database.Update("cmsPropertyData", "id", data, new[] { "dataNText" }); UpdateXmlTable(propertyTypes, data, cmsContentXmlEntries, database); @@ -91,11 +96,11 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven { var title = node.Attributes["title"].Value; var type = node.Attributes["type"].Value; - var newwindow = node.Attributes["newwindow"].Value.Equals("1") ? true : false; + var newwindow = node.Attributes["newwindow"].Value.Equals("1"); var lnk = node.Attributes["link"].Value; //create the links in the format the new prop editor expects it to be - var link = new ExpandoObject() as IDictionary; + var link = new ExpandoObject() as IDictionary; link.Add("title", title); link.Add("caption", title); link.Add("link", lnk); @@ -110,9 +115,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven } //store the serialized data - data.Text = JsonConvert.SerializeObject(links); + data.dataNtext = JsonConvert.SerializeObject(links); - database.Update(data); + database.Update("cmsPropertyData", "id", data, new[] { "dataNText" }); UpdateXmlTable(propertyTypes, data, cmsContentXmlEntries, database); @@ -127,20 +132,20 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven throw new DataLossException("Cannot downgrade from a version 7 database to a prior version, the database schema has already been modified"); } - private static void UpdateXmlTable(List propertyTypes, PropertyDataDto data, List cmsContentXmlEntries, Database database) + private static void UpdateXmlTable(List propertyTypes, dynamic data, List cmsContentXmlEntries, Database database) { //now we need to update the cmsContentXml table - var propertyType = propertyTypes.SingleOrDefault(x => x.Id == data.PropertyTypeId); + var propertyType = propertyTypes.SingleOrDefault(x => x.Id == data.propertytypeid); if (propertyType != null) { - var xmlItem = cmsContentXmlEntries.SingleOrDefault(x => x.NodeId == data.NodeId); + var xmlItem = cmsContentXmlEntries.SingleOrDefault(x => x.NodeId == data.contentNodeId); if (xmlItem != null) { var x = XElement.Parse(xmlItem.Xml); var prop = x.Element(propertyType.Alias); if (prop != null) { - prop.ReplaceAll(new XCData(data.Text)); + prop.ReplaceAll(new XCData(data.dataNtext)); database.Update(xmlItem); } } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs new file mode 100644 index 0000000000..6f9d74e5db --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveZero/EnsureServersLockObject.cs @@ -0,0 +1,57 @@ +using System; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveZero +{ + // This migration exists for 7.3.0 but it seems like it was not always running properly + // if you're upgrading from 7.3.0 or higher than we add this migration, if you're upgrading + // from 7.3.0 or lower then you will already get this migration in the migration to get to 7.3.0 + [Migration("7.3.0", "7.5.0", 10, GlobalSettings.UmbracoMigrationName)] + public class EnsureServersLockObject : MigrationBase + { + public EnsureServersLockObject(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + // that lock object should have been part of BaseDataCreation since 7.3.0 but + // for some reason it was not, so it was created during migrations but not during + // new installs, so for ppl that upgrade, make sure they have it + + EnsureLockObject(Constants.System.ServersLock, "0AF5E610-A310-4B6F-925F-E928D5416AF7", "LOCK: Servers"); + } + + public override void Down() + { + // not implemented + } + + private void EnsureLockObject(int id, string uniqueId, string text) + { + var exists = Context.Database.Exists(id); + if (exists) return; + + Insert + .IntoTable("umbracoNode") + .EnableIdentityInsert() + .Row(new + { + id = id, // NodeId + trashed = false, + parentId = -1, + nodeUser = 0, + level = 1, + path = "-1," + id, + sortOrder = 0, + uniqueId = new Guid(uniqueId), + text = text, + nodeObjectType = new Guid(Constants.ObjectTypes.LockObject), + createDate = DateTime.Now + }); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs new file mode 100644 index 0000000000..442b92d2b5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddDataDecimalColumn.cs @@ -0,0 +1,29 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero +{ + [Migration("7.4.0", 1, GlobalSettings.UmbracoMigrationName)] + public class AddDataDecimalColumn : MigrationBase + { + public AddDataDecimalColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + //Don't exeucte if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("cmsPropertyData") && x.ColumnName.InvariantEquals("dataDecimal")) == false) + Create.Column("dataDecimal").OnTable("cmsPropertyData").AsDecimal().Nullable(); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs new file mode 100644 index 0000000000..a39cea2ee0 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUmbracoDeployTables.cs @@ -0,0 +1,48 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero +{ + [Migration("7.4.0", 5, GlobalSettings.UmbracoMigrationName)] + public class AddUmbracoDeployTables : MigrationBase + { + public AddUmbracoDeployTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + //Don't exeucte if the table is already there + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoDeployChecksum")) return; + + Create.Table("umbracoDeployChecksum") + .WithColumn("id").AsInt32().Identity().PrimaryKey("PK_umbracoDeployChecksum") + .WithColumn("entityType").AsString(32).NotNullable() + .WithColumn("entityGuid").AsGuid().Nullable() + .WithColumn("entityPath").AsString(256).Nullable() + .WithColumn("localChecksum").AsString(32).NotNullable() + .WithColumn("compositeChecksum").AsString(32).Nullable(); + + Create.Table("umbracoDeployDependency") + .WithColumn("sourceId").AsInt32().NotNullable().ForeignKey("FK_umbracoDeployDependency_umbracoDeployChecksum_id1", "umbracoDeployChecksum", "id") + .WithColumn("targetId").AsInt32().NotNullable().ForeignKey("FK_umbracoDeployDependency_umbracoDeployChecksum_id2", "umbracoDeployChecksum", "id") + .WithColumn("mode").AsInt32().NotNullable(); + + Create.PrimaryKey("PK_umbracoDeployDependency").OnTable("umbracoDeployDependency").Columns(new[] {"sourceId", "targetId"}); + + Create.Index("IX_umbracoDeployChecksum").OnTable("umbracoDeployChecksum") + .OnColumn("entityType") + .Ascending() + .OnColumn("entityGuid") + .Ascending() + .OnColumn("entityPath") + .Unique(); + } + + public override void Down() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs new file mode 100644 index 0000000000..fe600f6b69 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/AddUniqueIdPropertyTypeGroupColumn.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero +{ + [Migration("7.4.0", 2, GlobalSettings.UmbracoMigrationName)] + public class AddUniqueIdPropertyTypeGroupColumn : MigrationBase + { + public AddUniqueIdPropertyTypeGroupColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + // don't execute if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("cmsPropertyTypeGroup") && x.ColumnName.InvariantEquals("uniqueID")) == false) + { + Create.Column("uniqueID").OnTable("cmsPropertyTypeGroup").AsGuid().NotNullable().WithDefault(SystemMethods.NewGuid); + + // unique constraint on name + version + Create.Index("IX_cmsPropertyTypeGroupUniqueID").OnTable("cmsPropertyTypeGroup") + .OnColumn("uniqueID").Ascending() + .WithOptions() + .NonClustered() + .WithOptions() + .Unique(); + + // fill in the data in a way that is consistent over all environments + // (ie cannot use random guids, http://issues.umbraco.org/issue/U4-6942) + Execute.Code(UpdateGuids); + } + } + + private static string UpdateGuids(Database database) + { + var updates = new List>(); + + foreach (var data in database.Query(@" +SELECT cmsPropertyTypeGroup.id grId, cmsPropertyTypeGroup.text grName, cmsContentType.alias ctAlias, umbracoNode.nodeObjectType nObjType +FROM cmsPropertyTypeGroup +INNER JOIN cmsContentType +ON cmsPropertyTypeGroup.contentTypeNodeId = cmsContentType.nodeId +INNER JOIN umbracoNode +ON cmsContentType.nodeId = umbracoNode.id")) + { + Guid guid; + // see BaseDataCreation... built-in groups have their own guids + if (data.grId == 3) + { + guid = new Guid(Constants.PropertyTypeGroups.Image); + } + else if (data.grId == 4) + { + guid = new Guid(Constants.PropertyTypeGroups.File); + } + else if (data.grId == 5) + { + guid = new Guid(Constants.PropertyTypeGroups.Contents); + } + else if (data.grId == 11) + { + guid = new Guid(Constants.PropertyTypeGroups.Membership); + } + else + { + // create a consistent guid from + // group name + content type alias + object type + string guidSource = data.grName + data.ctAlias + data.nObjType; + guid = guidSource.ToGuid(); + } + + // set the Unique Id to the one we've generated + // but not within the foreach loop (as we already have a data reader open) + updates.Add(Tuple.Create(guid, data.grId)); + } + + foreach (var update in updates) + database.Execute("UPDATE cmsPropertyTypeGroup SET uniqueID=@uid WHERE id=@id", new { uid = update.Item1, id = update.Item2 }); + + return string.Empty; + } + + public override void Down() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs new file mode 100644 index 0000000000..20200a3230 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/EnsureContentTypeUniqueIdsAreConsistent.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero +{ + /// + /// Courier on v. 7.4+ will handle ContentTypes using GUIDs instead of + /// alias, so we need to ensure that these are initially consistent on + /// all environments (based on the alias). + /// + [Migration("7.4.0", 3, GlobalSettings.UmbracoMigrationName)] + public class EnsureContentTypeUniqueIdsAreConsistent : MigrationBase + { + public EnsureContentTypeUniqueIdsAreConsistent(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var objectTypes = new[] + { + Constants.ObjectTypes.DocumentTypeGuid, + Constants.ObjectTypes.MediaTypeGuid, + Constants.ObjectTypes.MemberTypeGuid, + }; + + var sql = new Sql() + .Select("umbracoNode.id,cmsContentType.alias,umbracoNode.nodeObjectType") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId) + .WhereIn(x => x.NodeObjectType, objectTypes); + + var rows = Context.Database.Fetch(sql); + + foreach (var row in rows) + { + // create a consistent guid from + // alias + object type + var guidSource = ((string) row.alias) + ((Guid) row.nodeObjectType); + var guid = guidSource.ToGuid(); + + // set the Unique Id to the one we've generated + Update.Table("umbracoNode").Set(new { uniqueID = guid }).Where(new { id = row.id }); + } + } + + public override void Down() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs new file mode 100644 index 0000000000..e1fa9e9257 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/FixListViewMediaSortOrder.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero +{ + [Migration("7.4.0", 4, GlobalSettings.UmbracoMigrationName)] + public class FixListViewMediaSortOrder : MigrationBase + { + public FixListViewMediaSortOrder(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var mediaListviewIncludeProperties = Context.Database.Fetch(new Sql().Select("*").From(SqlSyntax).Where(x => x.Id == -9)).FirstOrDefault(); + if (mediaListviewIncludeProperties != null) + { + if (mediaListviewIncludeProperties.Value.Contains("\"alias\":\"sort\"")) + { + mediaListviewIncludeProperties.Value = mediaListviewIncludeProperties.Value.Replace("\"alias\":\"sort\"", "\"alias\":\"sortOrder\""); + Context.Database.InsertOrUpdate(mediaListviewIncludeProperties); + } + } + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs new file mode 100644 index 0000000000..3d285c2715 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFourZero/RemoveParentIdPropertyTypeGroupColumn.cs @@ -0,0 +1,40 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFourZero +{ + [Migration("7.4.0", 4, GlobalSettings.UmbracoMigrationName)] + public class RemoveParentIdPropertyTypeGroupColumn : MigrationBase + { + public RemoveParentIdPropertyTypeGroupColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + // don't execute if the column is not there anymore + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + + if (columns.Any(x => x.TableName.InvariantEquals("cmsPropertyTypeGroup") && x.ColumnName.InvariantEquals("parentGroupId")) == false) + return; + + //This constraing can be based on old aliases, before removing them, check they exist + var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); + if (constraints.Any(x => x.Item1.InvariantEquals("cmsPropertyTypeGroup") && x.Item3.InvariantEquals("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup_id"))) + { + Delete.ForeignKey("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup_id").OnTable("cmsPropertyTypeGroup"); + } + if (constraints.Any(x => x.Item1.InvariantEquals("cmsPropertyTypeGroup") && x.Item3.InvariantEquals("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup"))) + { + Delete.ForeignKey("FK_cmsPropertyTypeGroup_cmsPropertyTypeGroup").OnTable("cmsPropertyTypeGroup"); + } + + Delete.Column("parentGroupId").FromTable("cmsPropertyTypeGroup"); + } + + public override void Down() + { } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs new file mode 100644 index 0000000000..50f78ca66d --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeOne/UpdateUserLanguagesToIsoCode.cs @@ -0,0 +1,38 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeOne +{ + /// + /// This fixes the storage of user languages from the old format like en_us to en-US + /// + [Migration("7.3.1", 0, GlobalSettings.UmbracoMigrationName)] + public class UpdateUserLanguagesToIsoCode : MigrationBase + { + public UpdateUserLanguagesToIsoCode(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var userData = Context.Database.Fetch(new Sql().Select("*").From(SqlSyntax)); + foreach (var user in userData.Where(x => x.UserLanguage.Contains("_"))) + { + var languageParts = user.UserLanguage.Split('_'); + if (languageParts.Length == 2) + { + Update.Table("umbracoUser") + .Set(new {userLanguage = languageParts[0] + "-" + languageParts[1].ToUpperInvariant()}) + .Where(new {id = user.Id}); + } + } + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs new file mode 100644 index 0000000000..91b4bd6438 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeTwo/EnsureMigrationsTableIdentityIsCorrect.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeTwo +{ + /// + /// This reinserts all migrations in the migrations table to account for initial rows inserted + /// on creation without identity enabled. + /// + [Migration("7.3.2", 0, GlobalSettings.UmbracoMigrationName)] + public class EnsureMigrationsTableIdentityIsCorrect : MigrationBase + { + public EnsureMigrationsTableIdentityIsCorrect(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) + { + } + + public override void Up() + { + // Due to the delayed execution of migrations, we have to wrap this code in Execute.Code to ensure the previous + // migration steps (esp. creating the migrations table) have completed before trying to fetch migrations from + // this table. + List migrations = null; + Execute.Code(db => + { + migrations = Context.Database.Fetch(new Sql().Select("*").From(SqlSyntax)); + return string.Empty; + }); + Delete.FromTable("umbracoMigration").AllRows(); + Execute.Code(database => + { + if (migrations != null) + { + foreach (var migration in migrations) + { + database.Insert("umbracoMigration", "id", true, + new {name = migration.Name, createDate = migration.CreateDate, version = migration.Version}); + } + } + return string.Empty; + }); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs index f2f9261f52..7589c7000d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUniqueIdPropertyTypeColumn.cs @@ -5,14 +5,13 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero -{ +{ [Migration("7.3.0", 13, GlobalSettings.UmbracoMigrationName)] public class AddUniqueIdPropertyTypeColumn : MigrationBase { public AddUniqueIdPropertyTypeColumn(ISqlSyntaxProvider sqlSyntax, ILogger logger) : base(sqlSyntax, logger) - { - } + { } public override void Up() { @@ -23,7 +22,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe { Create.Column("uniqueID").OnTable("cmsPropertyType").AsGuid().NotNullable().WithDefault(SystemMethods.NewGuid); - //unique constraint on name + version + // unique constraint on name + version Create.Index("IX_cmsPropertyTypeUniqueID").OnTable("cmsPropertyType") .OnColumn("uniqueID").Ascending() .WithOptions() @@ -31,29 +30,29 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe .WithOptions() .Unique(); - //now we need to fill in the data so that it is consistent, we can't have it generating random GUIDs for - // the already existing data, see: http://issues.umbraco.org/issue/U4-6942 + // fill in the data in a way that is consistent over all environments + // (ie cannot use random guids, http://issues.umbraco.org/issue/U4-6942) foreach (var data in Context.Database.Query(@" -SELECT cmsPropertyType.id ptId, cmsPropertyType.Alias ptAlias, cmsContentType.alias ctAlias +SELECT cmsPropertyType.id ptId, cmsPropertyType.Alias ptAlias, cmsContentType.alias ctAlias, umbracoNode.nodeObjectType nObjType FROM cmsPropertyType INNER JOIN cmsContentType -ON cmsPropertyType.contentTypeId = cmsContentType.nodeId")) +ON cmsPropertyType.contentTypeId = cmsContentType.nodeId +INNER JOIN umbracoNode +ON cmsContentType.nodeId = umbracoNode.id")) { - //create a guid from the concatenation of the property type alias + the doc type alias - string concatAlias = data.ptAlias + data.ctAlias; - var ptGuid = concatAlias.ToGuid(); + // create a consistent guid from + // property alias + content type alias + object type + string guidSource = data.ptAlias + data.ctAlias + data.nObjType; + var guid = guidSource.ToGuid(); - //set the Unique Id to the one we've generated - Update.Table("cmsPropertyType").Set(new {uniqueID = ptGuid}).Where(new {id = data.ptId }); + // set the Unique Id to the one we've generated + Update.Table("cmsPropertyType").Set(new { uniqueID = guid }).Where(new { id = data.ptId }); } } - - } public override void Down() - { - } + { } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs index cb0ad58a49..01db36abd9 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MigrateAndRemoveTemplateMasterColumn.cs @@ -3,6 +3,7 @@ using System.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero @@ -118,6 +119,22 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe } + var dbIndexes = SqlSyntax.GetDefinedIndexes(Context.Database) + .Select(x => new DbIndexDefinition() + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + + //in some databases there's an index (IX_Master) on the master column which needs to be dropped first + var foundIndex = dbIndexes.FirstOrDefault(x => x.TableName.InvariantEquals("cmsTemplate") && x.ColumnName.InvariantEquals("master")); + if (foundIndex != null) + { + Delete.Index(foundIndex.IndexName).OnTable("cmsTemplate"); + } + if (cols.Any(x => x.ColumnName.InvariantEquals("master") && x.TableName.InvariantEquals("cmsTemplate"))) { Delete.Column("master").FromTable("cmsTemplate"); diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs index 6598719454..32c81700a8 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSixZeroOne/UpdatePropertyTypesAndGroups.cs @@ -31,58 +31,65 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSixZeroOne //Fetch all PropertyTypes that belongs to a PropertyTypeGroup //NOTE: We are writing the full query because we've added a column to the PropertyTypeDto in later versions so one of the columns // won't exist yet - var propertyTypes = database.Fetch("SELECT * FROM cmsPropertyType WHERE propertyTypeGroupId > 0"); + var propertyTypes = database.Fetch("SELECT * FROM cmsPropertyType WHERE propertyTypeGroupId > 0"); - var propertyGroups = database.Fetch("WHERE id > 0"); + // need to use dynamic, as PropertyTypeGroupDto has new properties + var propertyGroups = database.Fetch("SELECT * FROM cmsPropertyTypeGroup WHERE id > 0"); foreach (var propertyType in propertyTypes) { - //Get the PropertyTypeGroup that the current PropertyType references - var parentPropertyTypeGroup = propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyTypeGroupId); - if (parentPropertyTypeGroup != null) + // get the PropertyTypeGroup of the current PropertyType, skip if not found + var propertyTypeGroup = propertyGroups.FirstOrDefault(x => x.id == propertyType.propertyTypeGroupId); + if (propertyTypeGroup == null) continue; + + // if the PropretyTypeGroup belongs to the same content type as the PropertyType, then fine + if (propertyTypeGroup.contenttypeNodeId == propertyType.contentTypeId) continue; + + // else we want to assign the PropertyType to a proper PropertyTypeGroup + // ie one that does belong to the same content - look for it + var okPropertyTypeGroup = propertyGroups.FirstOrDefault(x => + x.text == propertyTypeGroup.text && // same name + x.contenttypeNodeId == propertyType.contentTypeId); // but for proper content type + + if (okPropertyTypeGroup == null) { - //If the ContentType is the same on the PropertyType and the PropertyTypeGroup the group is valid and we skip to the next - if (parentPropertyTypeGroup.ContentTypeNodeId == propertyType.ContentTypeId) continue; - - //Check if the 'new' PropertyTypeGroup has already been created - var existingPropertyTypeGroup = - propertyGroups.FirstOrDefault( - x => - x.ParentGroupId == parentPropertyTypeGroup.Id && x.Text == parentPropertyTypeGroup.Text && - x.ContentTypeNodeId == propertyType.ContentTypeId); - - //This should ensure that we don't create duplicate groups for a single ContentType - if (existingPropertyTypeGroup == null) + // does not exist, create a new PropertyTypeGroup + // cannot use a PropertyTypeGroupDto because of the new (not-yet-existing) uniqueID property + // cannot use a dynamic because database.Insert fails to set the value of property + var propertyGroup = new PropertyTypeGroupDtoTemp { + id = 0, + contenttypeNodeId = propertyType.contentTypeId, + text = propertyTypeGroup.text, + sortorder = propertyTypeGroup.sortorder + }; - //Create a new PropertyTypeGroup that references the parent group that the PropertyType was referencing pre-6.0.1 - var propertyGroup = new PropertyTypeGroupDto - { - ContentTypeNodeId = propertyType.ContentTypeId, - ParentGroupId = parentPropertyTypeGroup.Id, - Text = parentPropertyTypeGroup.Text, - SortOrder = parentPropertyTypeGroup.SortOrder - }; + // save + add to list of groups + int id = Convert.ToInt16(database.Insert("cmsPropertyTypeGroup", "id", propertyGroup)); + propertyGroup.id = id; + propertyGroups.Add(propertyGroup); - //Save the PropertyTypeGroup in the database and update the list of groups with this new group - int id = Convert.ToInt16(database.Insert(propertyGroup)); - propertyGroup.Id = id; - propertyGroups.Add(propertyGroup); - //Update the reference to the new PropertyTypeGroup on the current PropertyType - propertyType.PropertyTypeGroupId = id; - database.Update(propertyType); - } - else - { - //Update the reference to the existing PropertyTypeGroup on the current PropertyType - propertyType.PropertyTypeGroupId = existingPropertyTypeGroup.Id; - database.Update(propertyType); - } + // update the PropertyType to use the new PropertyTypeGroup + propertyType.propertyTypeGroupId = id; } + else + { + // exists, update PropertyType to use the PropertyTypeGroup + propertyType.propertyTypeGroupId = okPropertyTypeGroup.id; + } + database.Update("cmsPropertyType", "id", propertyType); } } return string.Empty; } + + private class PropertyTypeGroupDtoTemp + { + public int id { get; set; } + public int contenttypeNodeId { get; set; } + public string text { get; set; } + public int sortorder { get; set; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PetaPoco.cs b/src/Umbraco.Core/Persistence/PetaPoco.cs index d55ad8fc14..88f90639d1 100644 --- a/src/Umbraco.Core/Persistence/PetaPoco.cs +++ b/src/Umbraco.Core/Persistence/PetaPoco.cs @@ -834,7 +834,7 @@ namespace Umbraco.Core.Persistence var pd = PocoData.ForType(typeof(T)); try { - r = cmd.ExecuteReader(); + r = cmd.ExecuteReaderWithRetry(); OnExecutedCommand(cmd); } catch (Exception x) diff --git a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs index f3cbe5a583..4679b9b2ad 100644 --- a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs +++ b/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs @@ -128,6 +128,34 @@ namespace Umbraco.Core.Persistence.Querying right = Visit(b.Right); } } + else if (operand == "=") + { + // deal with (x == true|false) - most common + var constRight = b.Right as ConstantExpression; + if (constRight != null && constRight.Type == typeof (bool)) + return ((bool) constRight.Value) ? VisitNotNot(b.Left) : VisitNot(b.Left); + right = Visit(b.Right); + + // deal with (true|false == x) - why not + var constLeft = b.Left as ConstantExpression; + if (constLeft != null && constLeft.Type == typeof (bool)) + return ((bool) constLeft.Value) ? VisitNotNot(b.Right) : VisitNot(b.Right); + left = Visit(b.Left); + } + else if (operand == "<>") + { + // deal with (x != true|false) - most common + var constRight = b.Right as ConstantExpression; + if (constRight != null && constRight.Type == typeof(bool)) + return ((bool) constRight.Value) ? VisitNot(b.Left) : VisitNotNot(b.Left); + right = Visit(b.Right); + + // deal with (true|false != x) - why not + var constLeft = b.Left as ConstantExpression; + if (constLeft != null && constLeft.Type == typeof(bool)) + return ((bool) constLeft.Value) ? VisitNot(b.Right) : VisitNotNot(b.Right); + left = Visit(b.Left); + } else { left = Visit(b.Left); @@ -152,7 +180,7 @@ namespace Umbraco.Core.Persistence.Querying case "COALESCE": return string.Format("{0}({1},{2})", operand, left, right); default: - return left + " " + operand + " " + right; + return "(" + left + " " + operand + " " + right + ")"; } } @@ -231,25 +259,47 @@ namespace Umbraco.Core.Persistence.Querying switch (u.NodeType) { case ExpressionType.Not: - var o = Visit(u.Operand); - - //use a Not equal operator instead of <> since we don't know that <> works in all sql servers - - switch (u.Operand.NodeType) - { - case ExpressionType.MemberAccess: - //In this case it wil be a false property , i.e. x => !Trashed - SqlParameters.Add(true); - return string.Format("NOT ({0} = @0)", o); - default: - //In this case it could be anything else, such as: x => !x.Path.StartsWith("-20") - return string.Format("NOT ({0})", o); - } + return VisitNot(u.Operand); default: return Visit(u.Operand); } } + private string VisitNot(Expression exp) + { + var o = Visit(exp); + + // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers + // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // false property , i.e. x => !Trashed + SqlParameters.Add(true); + return string.Format("NOT ({0} = @{1})", o, SqlParameters.Count - 1); + default: + // could be anything else, such as: x => !x.Path.StartsWith("-20") + return "NOT (" + o + ")"; + } + } + + private string VisitNotNot(Expression exp) + { + var o = Visit(exp); + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // true property, i.e. x => Trashed + SqlParameters.Add(true); + return string.Format("({0} = @{1})", o, SqlParameters.Count - 1); + default: + // could be anything else, such as: x => x.Path.StartsWith("-20") + return o; + } + } + protected virtual string VisitNewArray(NewArrayExpression na) { diff --git a/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs b/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs index 859fa7cf5a..923348e729 100644 --- a/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs +++ b/src/Umbraco.Core/Persistence/Relators/UserSectionRelator.cs @@ -18,8 +18,11 @@ namespace Umbraco.Core.Persistence.Relators // Is this the same DictionaryItem as the current one we're processing if (Current != null && Current.Id == a.Id) { - // Yes, just add this User2AppDto to the current item's collection - Current.User2AppDtos.Add(p); + if (p.AppAlias.IsNullOrWhiteSpace() == false) + { + // Yes, just add this User2AppDto to the current item's collection + Current.User2AppDtos.Add(p); + } // Return null to indicate we're not done with this User yet return null; @@ -35,7 +38,7 @@ namespace Umbraco.Core.Persistence.Relators Current = a; Current.User2AppDtos = new List(); //this can be null since we are doing a left join - if (p.AppAlias != null) + if (p.AppAlias.IsNullOrWhiteSpace() == false) { Current.User2AppDtos.Add(p); } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 60d7d8e7ef..5f96cad114 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -105,6 +105,7 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of PetaPocoRepositoryBase + protected override Sql GetBaseQuery(bool isCount) { var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", @@ -155,8 +156,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM cmsContentVersion WHERE ContentId = @Id", "DELETE FROM cmsContentXml WHERE nodeId = @Id", "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id", - "DELETE FROM umbracoAccess WHERE nodeId = @Id" + "DELETE FROM umbracoAccess WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" }; return list; } @@ -234,7 +235,14 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -682,8 +690,7 @@ namespace Umbraco.Core.Persistence.Repositories public int CountPublished() { var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true) - .Where(x => x.Newest == true); + .Where(x => x.Published == true); return Database.ExecuteScalar(sql); } @@ -728,7 +735,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// public void AddOrUpdateContentXml(IContent content, Func xml) - { + { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } @@ -760,10 +767,11 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is @@ -782,7 +790,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -826,11 +834,11 @@ namespace Umbraco.Core.Persistence.Repositories var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) .ToArray(); - + var ids = dtos .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) .Select(x => x.TemplateId.Value).ToArray(); - + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); @@ -972,4 +980,4 @@ namespace Umbraco.Core.Persistence.Repositories _contentXmlRepository.Dispose(); } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 631bb7e1fb..dc85ad3a33 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -6,6 +6,8 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -16,6 +18,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -27,15 +30,60 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class ContentTypeBaseRepository : PetaPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { - protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { - _guidRepo = new GuidReadOnlyContentTypeBaseRepository(this, work, cache, logger, sqlSyntax); } - private readonly GuidReadOnlyContentTypeBaseRepository _guidRepo; + public IEnumerable> Move(TEntity toMove, EntityContainer container) + { + var parentId = Constants.System.Root; + if (container != null) + { + // Check on paths + if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); + } + parentId = container.Id; + } + //used to track all the moved entities to be given to the event + var moveInfo = new List> + { + new MoveEventInfo(toMove, toMove.Path, parentId) + }; + + + // get the level delta (old pos to new pos) + var levelDelta = container == null + ? 1 - toMove.Level + : container.Level + 1 - toMove.Level; + + // move to parent (or -1), update path, save + toMove.ParentId = parentId; + var toMovePath = toMove.Path + ","; // save before changing + toMove.Path = (container == null ? Constants.System.Root.ToString() : container.Path) + "," + toMove.Id; + toMove.Level = container == null ? 1 : container.Level + 1; + AddOrUpdate(toMove); + + //update all descendants, update in order of level + var descendants = GetByQuery(new Query().Where(type => type.Path.StartsWith(toMovePath))); + var paths = new Dictionary(); + paths[toMove.Id] = toMove.Path; + + foreach (var descendant in descendants.OrderBy(x => x.Level)) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; + descendant.Level += levelDelta; + + AddOrUpdate(descendant); + } + + return moveInfo; + } /// /// Returns the content type ids that match the query /// @@ -45,15 +93,15 @@ namespace Umbraco.Core.Persistence.Repositories { var sqlClause = new Sql(); sqlClause.Select("*") - .From() - .RightJoin() - .On(left => left.Id, right => right.PropertyTypeGroupId) - .InnerJoin() - .On(left => left.DataTypeId, right => right.DataTypeId); + .From(SqlSyntax) + .RightJoin(SqlSyntax) + .On(SqlSyntax, left => left.Id, right => right.PropertyTypeGroupId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.DataTypeId, right => right.DataTypeId); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() - .OrderBy(x => x.PropertyTypeGroupId); + .OrderBy(x => x.PropertyTypeGroupId, SqlSyntax); var dtos = Database.Fetch(new GroupPropertyTypeRelator().Map, sql); @@ -68,8 +116,11 @@ namespace Umbraco.Core.Persistence.Repositories return new PropertyType(propertyEditorAlias, dbType, propertyTypeAlias); } - protected void PersistNewBaseContentType(ContentTypeDto dto, IContentTypeComposition entity) + protected void PersistNewBaseContentType(IContentTypeComposition entity) { + var factory = new ContentTypeFactory(); + var dto = factory.BuildContentTypeDto(entity); + //Cannot add a duplicate content type type var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id @@ -181,38 +232,35 @@ AND umbracoNode.nodeObjectType = @objectType", } } - protected void PersistUpdatedBaseContentType(ContentTypeDto dto, IContentTypeComposition entity) + protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { + var factory = new ContentTypeFactory(); + var dto = factory.BuildContentTypeDto(entity); - //Cannot update to a duplicate alias + // ensure the alias is not used already var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id WHERE cmsContentType." + SqlSyntax.GetQuotedColumnName("alias") + @"= @alias AND umbracoNode.nodeObjectType = @objectType AND umbracoNode.id <> @id", - new { id = dto.NodeId, alias = entity.Alias, objectType = NodeObjectTypeId }); + new { id = dto.NodeId, alias = dto.Alias, objectType = NodeObjectTypeId }); if (exists > 0) - { - throw new DuplicateNameException("An item with the alias " + entity.Alias + " already exists"); - } - - var propertyGroupFactory = new PropertyGroupFactory(entity.Id); + throw new DuplicateNameException("An item with the alias " + dto.Alias + " already exists"); + // handle (update) the node var nodeDto = dto.NodeDto; - var o = Database.Update(nodeDto); + Database.Update(nodeDto); + // fixme - why? we are UPDATING so we should ALREADY have a PK! //Look up ContentType entry to get PrimaryKey for updating the DTO var dtoPk = Database.First("WHERE nodeId = @Id", new { Id = entity.Id }); dto.PrimaryKey = dtoPk.PrimaryKey; Database.Update(dto); - //Delete the ContentType composition entries before adding the updated collection + // handle (delete then recreate) compositions Database.Delete("WHERE childContentTypeId = @Id", new { Id = entity.Id }); - //Update ContentType composition in new table foreach (var composition in entity.ContentTypeComposition) - { Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); - } //Removing a ContentType from a composition (U4-1690) //1. Find content based on the current ContentType: entity.Id @@ -237,7 +285,7 @@ AND umbracoNode.id <> @id", { //Find PropertyTypes for the removed ContentType var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); - //Loop through the Content that is based on the current ContentType in order to remove the Properties that are + //Loop through the Content that is based on the current ContentType in order to remove the Properties that are //based on the PropertyTypes that belong to the removed ContentType. foreach (var contentDto in contentDtos) { @@ -291,54 +339,43 @@ AND umbracoNode.id <> @id", } } - if (entity.IsPropertyDirty("PropertyGroups") || - entity.PropertyGroups.Any(x => x.IsDirty())) + if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) { - //Delete Tabs/Groups by excepting entries from db with entries from collections - var dbPropertyGroups = - Database.Fetch("WHERE contenttypeNodeId = @Id", new { Id = entity.Id }) - .Select(x => new Tuple(x.Id, x.Text)) - .ToList(); - var entityPropertyGroups = entity.PropertyGroups.Select(x => new Tuple(x.Id, x.Name)).ToList(); - var tabsToDelete = dbPropertyGroups.Select(x => x.Item1).Except(entityPropertyGroups.Select(x => x.Item1)); - var tabs = dbPropertyGroups.Where(x => tabsToDelete.Any(y => y == x.Item1)); - //Update Tab name downstream to ensure renaming is done properly - foreach (var propertyGroup in entityPropertyGroups) + // todo + // we used to try to propagate tabs renaming downstream, relying on ParentId, but + // 1) ParentId makes no sense (if a tab can be inherited from multiple composition + // types) so we would need to figure things out differently, visiting downstream + // content types and looking for tabs with the same name... + // 2) It was not deployable as changing a content type changes other content types + // that was not deterministic, because it would depend on the order of the changes. + // That last point could be fixed if (1) is fixed, but then it still is an issue with + // deploy because changing a content type changes other content types that are not + // dependencies but dependents, and then what? + // + // So... for the time being, all renaming propagation is disabled. We just don't do it. + + // (all gone) + + // delete tabs that do not exist anymore + // get the tabs that are currently existing (in the db) + // get the tabs that we want, now + // and derive the tabs that we want to delete + var existingPropertyGroups = Database.Fetch("WHERE contentTypeNodeId = @id", new { id = entity.Id }) + .Select(x => x.Id) + .ToList(); + var newPropertyGroups = entity.PropertyGroups.Select(x => x.Id).ToList(); + var tabsToDelete = existingPropertyGroups + .Except(newPropertyGroups) + .ToArray(); + + // move properties to generic properties, and delete the tabs + if (tabsToDelete.Length > 0) { - Database.Update("SET Text = @TabName WHERE parentGroupId = @TabId", - new { TabName = propertyGroup.Item2, TabId = propertyGroup.Item1 }); - - var childGroups = Database.Fetch("WHERE parentGroupId = @TabId", new { TabId = propertyGroup.Item1 }); - foreach (var childGroup in childGroups) - { - var sibling = Database.Fetch("WHERE contenttypeNodeId = @Id AND text = @Name", - new { Id = childGroup.ContentTypeNodeId, Name = propertyGroup.Item2 }) - .FirstOrDefault(x => x.ParentGroupId.HasValue == false || x.ParentGroupId.Value.Equals(propertyGroup.Item1) == false); - //If the child group doesn't have a sibling there is no chance of duplicates and we continue - if (sibling == null || (sibling.ParentGroupId.HasValue && sibling.ParentGroupId.Value.Equals(propertyGroup.Item1))) continue; - - //Since the child group has a sibling with the same name we need to point any PropertyTypes to the sibling - //as this child group is about to leave the party. - Database.Update( - "SET propertyTypeGroupId = @PropertyTypeGroupId WHERE propertyTypeGroupId = @PropertyGroupId AND ContentTypeId = @ContentTypeId", - new { PropertyTypeGroupId = sibling.Id, PropertyGroupId = childGroup.Id, ContentTypeId = childGroup.ContentTypeNodeId }); - - //Since the parent group has been renamed and we have duplicates we remove this group - //and leave our sibling in charge of the part. - Database.Delete(childGroup); - } - } - //Do Tab updates - foreach (var tab in tabs) - { - Database.Update("SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId = @PropertyGroupId", - new { PropertyGroupId = tab.Item1 }); - Database.Update("SET parentGroupId = NULL WHERE parentGroupId = @TabId", - new { TabId = tab.Item1 }); - Database.Delete("WHERE contenttypeNodeId = @Id AND text = @Name", - new { Id = entity.Id, Name = tab.Item2 }); + Database.Update("SET propertyTypeGroupId=NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = tabsToDelete }); + Database.Delete("WHERE id IN (@ids)", new { ids = tabsToDelete }); } } + var propertyGroupFactory = new PropertyGroupFactory(entity.Id); //Run through all groups to insert or update entries foreach (var propertyGroup in entity.PropertyGroups) @@ -372,7 +409,7 @@ AND umbracoNode.id <> @id", AssignDataTypeFromPropertyEditor(propertyType); } - //validate the alias! + //validate the alias! ValidateAlias(propertyType); var propertyTypeDto = propertyGroupFactory.BuildPropertyTypeDto(tabId, propertyType); @@ -382,25 +419,6 @@ AND umbracoNode.id <> @id", if (propertyType.HasIdentity == false) propertyType.Id = typePrimaryKey; //Set Id on new PropertyType } - - //If a Composition is removed we need to update/reset references to the PropertyGroups on that ContentType - if (entity.IsPropertyDirty("ContentTypeComposition") && - compositionBase != null && - compositionBase.RemovedContentTypeKeyTracker != null && - compositionBase.RemovedContentTypeKeyTracker.Any()) - { - foreach (var compositionId in compositionBase.RemovedContentTypeKeyTracker) - { - var dbPropertyGroups = - Database.Fetch("WHERE contenttypeNodeId = @Id", new { Id = compositionId }) - .Select(x => x.Id); - foreach (var propertyGroup in dbPropertyGroups) - { - Database.Update("SET parentGroupId = NULL WHERE parentGroupId = @TabId AND contenttypeNodeId = @ContentTypeNodeId", - new { TabId = propertyGroup, ContentTypeNodeId = entity.Id }); - } - } - } } protected IEnumerable GetAllowedContentTypeIds(int id) @@ -454,6 +472,7 @@ AND umbracoNode.id <> @id", propType.DataTypeDefinitionId = dto.DataTypeId; propType.Description = dto.Description; propType.Id = dto.Id; + propType.Key = dto.UniqueId; propType.Name = dto.Name; propType.Mandatory = dto.Mandatory; propType.SortOrder = dto.SortOrder; @@ -564,14 +583,13 @@ AND umbracoNode.id <> @id", } } - public static IEnumerable GetMediaTypes( - TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetMediaTypes( + Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository) - where TRepo : IReadRepository - where TId: struct + where TRepo : IReadRepository { - IDictionary> allParentMediaTypeIds; - var mediaTypes = MapMediaTypes(mediaTypeIds, db, sqlSyntax, out allParentMediaTypeIds) + IDictionary> allParentMediaTypeIds; + var mediaTypes = MapMediaTypes(db, sqlSyntax, out allParentMediaTypeIds) .ToArray(); MapContentTypeChildren(mediaTypes, db, sqlSyntax, contentTypeRepository, allParentMediaTypeIds); @@ -579,16 +597,15 @@ AND umbracoNode.id <> @id", return mediaTypes; } - public static IEnumerable GetContentTypes( - TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetContentTypes( + Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, ITemplateRepository templateRepository) - where TRepo : IReadRepository - where TId : struct + where TRepo : IReadRepository { - IDictionary> allAssociatedTemplates; - IDictionary> allParentContentTypeIds; - var contentTypes = MapContentTypes(contentTypeIds, db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) + IDictionary> allAssociatedTemplates; + IDictionary> allParentContentTypeIds; + var contentTypes = MapContentTypes(db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) .ToArray(); if (contentTypes.Any()) @@ -603,12 +620,11 @@ AND umbracoNode.id <> @id", return contentTypes; } - internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, + internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, - IDictionary> allParentContentTypeIds) - where TRepo : IReadRepository - where TId : struct + IDictionary> allParentContentTypeIds) + where TRepo : IReadRepository { //NOTE: SQL call #2 @@ -620,7 +636,7 @@ AND umbracoNode.id <> @id", foreach (var contentType in contentTypes) { contentType.PropertyGroups = allPropGroups[contentType.Id]; - ((ContentTypeBase)contentType).PropertyTypes = allPropTypes[contentType.Id]; + contentType.NoGroupPropertyTypes = allPropTypes[contentType.Id]; } //NOTE: SQL call #3++ @@ -630,25 +646,22 @@ AND umbracoNode.id <> @id", var allParentIdsAsArray = allParentContentTypeIds.SelectMany(x => x.Value).Distinct().ToArray(); if (allParentIdsAsArray.Any()) { - var allParentContentTypes = contentTypeRepository.GetAll(allParentIdsAsArray).ToArray(); + var allParentContentTypes = contentTypes.Where(x => allParentIdsAsArray.Contains(x.Id)).ToArray(); + foreach (var contentType in contentTypes) - { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; + { + var entityId = contentType.Id; var parentContentTypes = allParentContentTypes.Where(x => { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var parentEntityId = typeof(TId) == typeof(int) ? x.Id : (object)x.Key; + var parentEntityId = x.Id; - return allParentContentTypeIds[(TId)entityId].Contains((TId)parentEntityId); + return allParentContentTypeIds[entityId].Contains(parentEntityId); }); foreach (var parentContentType in parentContentTypes) { var result = contentType.AddContentType(parentContentType); - //Do something if adding fails? (Should hopefully not be possible unless someone created a circular reference) + //Do something if adding fails? (Should hopefully not be possible unless someone created a circular reference) } //on initial construction we don't want to have dirty properties tracked @@ -661,13 +674,12 @@ AND umbracoNode.id <> @id", } - internal static void MapContentTypeTemplates(IContentType[] contentTypes, + internal static void MapContentTypeTemplates(IContentType[] contentTypes, Database db, TRepo contentTypeRepository, ITemplateRepository templateRepository, - IDictionary> associatedTemplates) - where TRepo : IReadRepository - where TId: struct + IDictionary> associatedTemplates) + where TRepo : IReadRepository { if (associatedTemplates == null || associatedTemplates.Any() == false) return; @@ -684,11 +696,9 @@ AND umbracoNode.id <> @id", foreach (var contentType in contentTypes) { - //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids - // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be - var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; - - var associatedTemplateIds = associatedTemplates[(TId)entityId].Select(x => x.TemplateId) + var entityId = contentType.Id; + + var associatedTemplateIds = associatedTemplates[entityId].Select(x => x.TemplateId) .Distinct() .ToArray(); @@ -700,19 +710,14 @@ AND umbracoNode.id <> @id", } - internal static IEnumerable MapMediaTypes(TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> parentMediaTypeIds) - where TId : struct + internal static IEnumerable MapMediaTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> parentMediaTypeIds) { - Mandate.That(mediaTypeIds.Any(), () => new InvalidOperationException("must be at least one content type id specified")); Mandate.ParameterNotNull(db, "db"); - - //ensure they are unique - mediaTypeIds = mediaTypeIds.Distinct().ToArray(); - + var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.allowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId, ParentTypes.parentContentTypeKey as chtParentKey, umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, @@ -721,40 +726,23 @@ AND umbracoNode.id <> @id", INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType INNER JOIN cmsContentType ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId ) AllowedTypes ON AllowedTypes.Id = cmsContentType.nodeId LEFT JOIN ( SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId - FROM cmsContentType2ContentType + FROM cmsContentType2ContentType INNER JOIN umbracoNode ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" - ) ParentTypes - ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - - if (mediaTypeIds.Any()) - { - //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will - // work for the time being. - if (typeof(TId) == typeof(int)) - { - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; - } - else if (typeof(TId) == typeof(Guid)) - { - sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; - } - } - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - if ((mediaTypeIds.Length - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType), contentTypeIds = mediaTypeIds }); + ) ParentTypes + ON ParentTypes.childContentTypeId = cmsContentType.nodeId + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); if (result.Any() == false) { @@ -762,95 +750,118 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentMediaTypeIds = new Dictionary>(); + parentMediaTypeIds = new Dictionary>(); var mappedMediaTypes = new List(); - foreach (var contentTypeId in mediaTypeIds) + //loop through each result and fill in our required values, each row will contain different requried data than the rest. + // it is much quicker to iterate each result and populate instead of looking up the values over and over in the result like + // we used to do. + var queue = new Queue(result); + var currAllowedContentTypes = new List(); + + while (queue.Count > 0) { - //the current content type id that we're working with + var ct = queue.Dequeue(); - var currentCtId = contentTypeId; - - //first we want to get the main content type data this is 1 : 1 with umbraco node data - - var ct = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof (TId) == typeof (int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) - .DistinctBy(x => (int)x.ctId) - .FirstOrDefault(); - - if (ct == null) + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) { - continue; + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) + { + currAllowedContentTypes.Add(ctSort); + } } - var contentTypeDto = new ContentTypeDto + //always ensure there's a list for this content type + if (parentMediaTypeIds.ContainsKey(ct.ctId) == false) + parentMediaTypeIds[ct.ctId] = new List(); + + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) { - Alias = ct.ctAlias, - AllowAtRoot = ct.ctAllowAtRoot, - Description = ct.ctDesc, - Icon = ct.ctIcon, - IsContainer = ct.ctIsContainer, - NodeId = ct.ctId, - PrimaryKey = ct.ctPk, - Thumbnail = ct.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = ct.nCreateDate, - Level = (short)ct.nLevel, - NodeId = ct.ctId, - NodeObjectType = ct.nObjectType, - ParentId = ct.nParentId, - Path = ct.nPath, - SortOrder = ct.nSortOrder, - Text = ct.nName, - Trashed = ct.nTrashed, - UniqueId = ct.nUniqueId, - UserId = ct.nUser - } - }; + var associatedParentIds = parentMediaTypeIds[ct.ctId]; + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } - //now create the media type object + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var mediaType = CreateForMapping(ct, currAllowedContentTypes); + mappedMediaTypes.Add(mediaType); - var factory = new MediaTypeFactory(new Guid(Constants.ObjectTypes.MediaType)); - var mediaType = factory.BuildEntity(contentTypeDto); - - //map the allowed content types - //map the child content type ids - MapCommonContentTypeObjects(mediaType, currentCtId, result, parentMediaTypeIds); - - mappedMediaTypes.Add(mediaType); + //Here we need to reset the current variables, we're now collecting data for a different content type + currAllowedContentTypes = new List(); + } } return mappedMediaTypes; } - internal static IEnumerable MapContentTypes(TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> associatedTemplates, - out IDictionary> parentContentTypeIds) - where TId : struct + private static IMediaType CreateForMapping(dynamic currCt, List currAllowedContentTypes) + { + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list + + var contentTypeDto = new ContentTypeDto + { + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }; + + //now create the content type object + + var factory = new ContentTypeFactory(); + var mediaType = factory.BuildMediaTypeEntity(contentTypeDto); + + //map the allowed content types + mediaType.AllowedContentTypes = currAllowedContentTypes; + + return mediaType; + } + + internal static IEnumerable MapContentTypes(Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> associatedTemplates, + out IDictionary> parentContentTypeIds) { Mandate.ParameterNotNull(db, "db"); - - //ensure they are unique - contentTypeIds = contentTypeIds.Distinct().ToArray(); - + var sql = @"SELECT cmsDocumentType.IsDefault as dtIsDefault, cmsDocumentType.templateNodeId as dtTemplateId, cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, - AllowedTypes.allowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, + AllowedTypes.AllowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId,ParentTypes.parentContentTypeKey as chtParentKey, umbracoNode.createDate as nCreateDate, umbracoNode." + sqlSyntax.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, umbracoNode.parentID as nParentId, umbracoNode." + sqlSyntax.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + sqlSyntax.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, - umbracoNode.uniqueID as nUniqueId, + umbracoNode.uniqueID as nUniqueId, Template.alias as tAlias, Template.nodeId as tId,Template.text as tText FROM cmsContentType INNER JOIN umbracoNode @@ -858,8 +869,8 @@ AND umbracoNode.id <> @id", LEFT JOIN cmsDocumentType ON cmsDocumentType.contentTypeNodeId = cmsContentType.nodeId LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType INNER JOIN cmsContentType ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId ) AllowedTypes @@ -872,33 +883,15 @@ AND umbracoNode.id <> @id", ON Template.nodeId = cmsDocumentType.templateNodeId LEFT JOIN ( SELECT cmsContentType2ContentType.parentContentTypeId, umbracoNode.uniqueID AS parentContentTypeKey, cmsContentType2ContentType.childContentTypeId - FROM cmsContentType2ContentType + FROM cmsContentType2ContentType INNER JOIN umbracoNode ON cmsContentType2ContentType.parentContentTypeId = umbracoNode." + sqlSyntax.GetQuotedColumnName("id") + @" - ) ParentTypes - ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - - if (contentTypeIds.Any()) - { - //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will - // work for the time being. - if (typeof(TId) == typeof(int)) - { - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; - } - else if (typeof(TId) == typeof(Guid)) - { - sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; - } - } - - - //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! - if ((contentTypeIds.Length - 1) > 2000) - throw new InvalidOperationException("Cannot perform this lookup, too many sql parameters"); - - var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType), contentTypeIds = contentTypeIds }); + ) ParentTypes + ON ParentTypes.childContentTypeId = cmsContentType.nodeId + WHERE (umbracoNode.nodeObjectType = @nodeObjectType) + ORDER BY ctId"; + + var result = db.Fetch(sql, new { nodeObjectType = new Guid(Constants.ObjectTypes.DocumentType)}); if (result.Any() == false) { @@ -907,163 +900,139 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentContentTypeIds = new Dictionary>(); - associatedTemplates = new Dictionary>(); + parentContentTypeIds = new Dictionary>(); + associatedTemplates = new Dictionary>(); var mappedContentTypes = new List(); - foreach (var contentTypeId in contentTypeIds) + var queue = new Queue(result); + var currDefaultTemplate = -1; + var currAllowedContentTypes = new List(); + while (queue.Count > 0) { - //the current content type id that we're working with + var ct = queue.Dequeue(); - var currentCtId = contentTypeId; - - //first we want to get the main content type data this is 1 : 1 with umbraco node data - - var ct = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) - .DistinctBy(x => (int)x.ctId) - .FirstOrDefault(); - - if (ct == null) + //check for default templates + bool? isDefaultTemplate = Convert.ToBoolean(ct.dtIsDefault); + int? templateId = ct.dtTemplateId; + if (currDefaultTemplate == -1 && isDefaultTemplate.HasValue && isDefaultTemplate.Value && templateId.HasValue) { - continue; + currDefaultTemplate = templateId.Value; } - //get the unique list of associated templates - var defaultTemplates = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - //use a tuple so that distinct checks both values (in some rare cases the dtIsDefault will not compute as bool?, so we force it with Convert.ToBoolean) - .Select(x => new Tuple(Convert.ToBoolean(x.dtIsDefault), x.dtTemplateId)) - .Where(x => x.Item1.HasValue && x.Item2.HasValue) - .Distinct() - .OrderByDescending(x => x.Item1.Value) - .ToArray(); - //if there isn't one set to default explicitly, we'll pick the first one - var defaultTemplate = defaultTemplates.FirstOrDefault(x => x.Item1.Value) - ?? defaultTemplates.FirstOrDefault(); + //always ensure there's a list for this content type + if (associatedTemplates.ContainsKey(ct.ctId) == false) + associatedTemplates[ct.ctId] = new List(); - var dtDto = new DocumentTypeDto + //check for associated templates and assign to the outgoing collection + if (ct.tId != null) { - //create the content type dto - ContentTypeDto = new ContentTypeDto + var associatedTemplate = new AssociatedTemplate(ct.tId, ct.tAlias, ct.tText); + var associatedList = associatedTemplates[ct.ctId]; + + if (associatedList.Contains(associatedTemplate) == false) + associatedList.Add(associatedTemplate); + } + + //check for allowed content types + int? allowedCtId = ct.ctaAllowedId; + int? allowedCtSort = ct.ctaSortOrder; + string allowedCtAlias = ct.ctaAlias; + if (allowedCtId.HasValue && allowedCtSort.HasValue && allowedCtAlias != null) + { + var ctSort = new ContentTypeSort(new Lazy(() => allowedCtId.Value), allowedCtSort.Value, allowedCtAlias); + if (currAllowedContentTypes.Contains(ctSort) == false) { - Alias = ct.ctAlias, - AllowAtRoot = ct.ctAllowAtRoot, - Description = ct.ctDesc, - Icon = ct.ctIcon, - IsContainer = ct.ctIsContainer, - NodeId = ct.ctId, - PrimaryKey = ct.ctPk, - Thumbnail = ct.ctThumb, - //map the underlying node dto - NodeDto = new NodeDto - { - CreateDate = ct.nCreateDate, - Level = (short)ct.nLevel, - NodeId = ct.ctId, - NodeObjectType = ct.nObjectType, - ParentId = ct.nParentId, - Path = ct.nPath, - SortOrder = ct.nSortOrder, - Text = ct.nName, - Trashed = ct.nTrashed, - UniqueId = ct.nUniqueId, - UserId = ct.nUser - } - }, - ContentTypeNodeId = ct.ctId, - IsDefault = defaultTemplate != null, - TemplateNodeId = defaultTemplate != null ? defaultTemplate.Item2.Value : 0, - }; + currAllowedContentTypes.Add(ctSort); + } + } - // We will map a subset of the associated template - alias, id, name + //always ensure there's a list for this content type + if (parentContentTypeIds.ContainsKey(ct.ctId) == false) + parentContentTypeIds[ct.ctId] = new List(); - associatedTemplates.Add(currentCtId, result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Where(x => x.tId != null) - .Select(x => new AssociatedTemplate(x.tId, x.tAlias, x.tText)) - .Distinct() - .ToArray()); + //check for parent ids and assign to the outgoing collection + int? parentId = ct.chtParentId; + if (parentId.HasValue) + { + var associatedParentIds = parentContentTypeIds[ct.ctId]; - //now create the content type object + if (associatedParentIds.Contains(parentId.Value) == false) + associatedParentIds.Add(parentId.Value); + } - var factory = new ContentTypeFactory(new Guid(Constants.ObjectTypes.DocumentType)); - var contentType = factory.BuildEntity(dtDto); + if (queue.Count == 0 || queue.Peek().ctId != ct.ctId) + { + //it's the last in the queue or the content type is changing (moving to the next one) + var contentType = CreateForMapping(ct, currAllowedContentTypes, currDefaultTemplate); + mappedContentTypes.Add(contentType); - //map the allowed content types - //map the child content type ids - MapCommonContentTypeObjects(contentType, currentCtId, result, parentContentTypeIds); - - mappedContentTypes.Add(contentType); + //Here we need to reset the current variables, we're now collecting data for a different content type + currDefaultTemplate = -1; + currAllowedContentTypes = new List(); + } } return mappedContentTypes; } - private static void MapCommonContentTypeObjects(T contentType, TId currentCtId, List result, IDictionary> parentContentTypeIds) - where T : IContentTypeBase - where TId : struct + private static IContentType CreateForMapping(dynamic currCt, List currAllowedContentTypes, int currDefaultTemplate) { - //map the allowed content types - contentType.AllowedContentTypes = result - .Where(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - //use tuple so we can use distinct on all vals - .Select(x => new Tuple(x.ctaAllowedId, x.ctaSortOrder, x.ctaAlias)) - .Where(x => x.Item1.HasValue && x.Item2.HasValue && x.Item3 != null) - .Distinct() - .Select(x => new ContentTypeSort(new Lazy(() => x.Item1.Value), x.Item2.Value, x.Item3)) - .ToList(); + // * set the default template to the first one if a default isn't found + // * create the DTO object + // * create the content type object + // * map the allowed content types + // * add to the outgoing list - //map the child content type ids - parentContentTypeIds.Add(currentCtId, result - .Where(x => + var dtDto = new ContentTypeTemplateDto + { + //create the content type dto + ContentTypeDto = new ContentTypeDto { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? x.ctId == currentCtId - : x.nUniqueId == currentCtId; - }) - .Select(x => - { - //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is - // how it is for now. - return (typeof(TId) == typeof(int)) - ? (TId?)x.chtParentId - : (TId?)x.chtParentKey; - }) - .Where(x => x.HasValue) - .Distinct() - .Select(x => x.Value).ToList()); + Alias = currCt.ctAlias, + AllowAtRoot = currCt.ctAllowAtRoot, + Description = currCt.ctDesc, + Icon = currCt.ctIcon, + IsContainer = currCt.ctIsContainer, + NodeId = currCt.ctId, + PrimaryKey = currCt.ctPk, + Thumbnail = currCt.ctThumb, + //map the underlying node dto + NodeDto = new NodeDto + { + CreateDate = currCt.nCreateDate, + Level = (short)currCt.nLevel, + NodeId = currCt.ctId, + NodeObjectType = currCt.nObjectType, + ParentId = currCt.nParentId, + Path = currCt.nPath, + SortOrder = currCt.nSortOrder, + Text = currCt.nName, + Trashed = currCt.nTrashed, + UniqueId = currCt.nUniqueId, + UserId = currCt.nUser + } + }, + ContentTypeNodeId = currCt.ctId, + IsDefault = currDefaultTemplate != -1, + TemplateNodeId = currDefaultTemplate != -1 ? currDefaultTemplate : 0, + }; + + //now create the content type object + + var factory = new ContentTypeFactory(); + var contentType = factory.BuildContentTypeEntity(dtDto.ContentTypeDto); + + // NOTE + // that was done by the factory but makes little sense, moved here, so + // now we have to reset dirty props again (as the factory does it) and yet, + // we are not managing allowed templates... the whole thing is weird. + ((ContentType)contentType).DefaultTemplateId = dtDto.TemplateNodeId; + contentType.ResetDirtyProperties(false); + + //map the allowed content types + contentType.AllowedContentTypes = currAllowedContentTypes; + + return contentType; } internal static void MapGroupsAndProperties(int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, @@ -1081,14 +1050,14 @@ AND umbracoNode.id <> @id", // therefore the union of the two contains all of the property type and property group information we need // NOTE: MySQL requires a SELECT * FROM the inner union in order to be able to sort . lame. - var sqlBuilder = new StringBuilder(@"SELECT PG.contenttypeNodeId as contentTypeId, - PT.ptUniqueId as ptUniqueID, PT.ptId, PT.ptAlias, PT.ptDesc,PT.ptMandatory,PT.ptName,PT.ptSortOrder,PT.ptRegExp, + var sqlBuilder = new StringBuilder(@"SELECT PG.contenttypeNodeId as contentTypeId, + PT.ptUniqueId as ptUniqueID, PT.ptId, PT.ptAlias, PT.ptDesc,PT.ptMandatory,PT.ptName,PT.ptSortOrder,PT.ptRegExp, PT.dtId,PT.dtDbType,PT.dtPropEdAlias, - PG.id as pgId, PG.parentGroupId as pgParentGroupId, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText + PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText FROM cmsPropertyTypeGroup as PG LEFT JOIN ( - SELECT PT.uniqueID as ptUniqueId, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, + SELECT PT.uniqueID as ptUniqueId, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, PT.propertyTypeGroupId as ptGroupId, DT.dbType as dtDbType, DT.nodeId as dtId, DT.propertyEditorAlias as dtPropEdAlias @@ -1098,14 +1067,14 @@ AND umbracoNode.id <> @id", ) as PT ON PT.ptGroupId = PG.id WHERE (PG.contenttypeNodeId in (@contentTypeIds)) - + UNION SELECT PT.contentTypeId as contentTypeId, - PT.uniqueID as ptUniqueID, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, + PT.uniqueID as ptUniqueID, PT.id as ptId, PT.Alias as ptAlias, PT." + sqlSyntax.GetQuotedColumnName("Description") + @" as ptDesc, PT.mandatory as ptMandatory, PT.Name as ptName, PT.sortOrder as ptSortOrder, PT.validationRegExp as ptRegExp, DT.nodeId as dtId, DT.dbType as dtDbType, DT.propertyEditorAlias as dtPropEdAlias, - PG.id as pgId, PG.parentGroupId as pgParentGroupId, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText + PG.id as pgId, PG.uniqueID as pgKey, PG.sortorder as pgSortOrder, PG." + sqlSyntax.GetQuotedColumnName("text") + @" as pgText FROM cmsPropertyType as PT INNER JOIN cmsDataType as DT ON PT.dataTypeId = DT.nodeId @@ -1138,7 +1107,7 @@ AND umbracoNode.id <> @id", //filter based on the current content type .Where(x => x.contentTypeId == currId) //turn that into a custom object containing only the group info - .Select(x => new { GroupId = x.pgId, ParentGroupId = x.pgParentGroupId, SortOrder = x.pgSortOrder, Text = x.pgText }) + .Select(x => new { GroupId = x.pgId, SortOrder = x.pgSortOrder, Text = x.pgText, Key = x.pgKey }) //get distinct data by id .DistinctBy(x => (int)x.GroupId) //for each of these groups, create a group object with it's associated properties @@ -1162,8 +1131,8 @@ AND umbracoNode.id <> @id", //fill in the rest of the group properties Id = group.GroupId, Name = group.Text, - ParentId = group.ParentGroupId, - SortOrder = group.SortOrder + SortOrder = group.SortOrder, + Key = group.Key }).ToArray()); allPropertyGroupCollection[currId] = propertyGroupCollection; @@ -1196,71 +1165,20 @@ AND umbracoNode.id <> @id", } - /// - /// Inner repository to support the GUID lookups and keep the caching consistent - /// - internal class GuidReadOnlyContentTypeBaseRepository : PetaPocoRepositoryBase - { - private readonly ContentTypeBaseRepository _parentRepo; - - public GuidReadOnlyContentTypeBaseRepository( - ContentTypeBaseRepository parentRepo, - IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, cache, logger, sqlSyntax) - { - _parentRepo = parentRepo; - } - - protected override TEntity PerformGet(Guid id) - { - return _parentRepo.PerformGet(id); - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - return _parentRepo.PerformGetAll(ids); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _parentRepo.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.uniqueID = @Id"; - } - - #region No implementation required - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotImplementedException(); - } - - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } - - protected override Guid NodeObjectTypeId - { - get { throw new NotImplementedException(); } - } - - protected override void PersistNewItem(TEntity entity) - { - throw new NotImplementedException(); - } - - protected override void PersistUpdatedItem(TEntity entity) - { - throw new NotImplementedException(); - } - #endregion - } - protected abstract TEntity PerformGet(Guid id); + protected abstract TEntity PerformGet(string alias); protected abstract IEnumerable PerformGetAll(params Guid[] ids); + protected abstract bool PerformExists(Guid id); + + /// + /// Gets an Entity by alias + /// + /// + /// + public TEntity Get(string alias) + { + return PerformGet(alias); + } /// /// Gets an Entity by Id @@ -1269,7 +1187,7 @@ AND umbracoNode.id <> @id", /// public TEntity Get(Guid id) { - return _guidRepo.Get(id); + return PerformGet(id); } /// @@ -1277,9 +1195,12 @@ AND umbracoNode.id <> @id", /// /// /// - public IEnumerable GetAll(params Guid[] ids) + /// + /// Ensure explicit implementation, we don't want to have any accidental calls to this since it is essentially the same signature as the main GetAll when there are no parameters + /// + IEnumerable IReadRepository.GetAll(params Guid[] ids) { - return _guidRepo.GetAll(ids); + return PerformGetAll(ids); } /// @@ -1289,7 +1210,21 @@ AND umbracoNode.id <> @id", /// public bool Exists(Guid id) { - return _guidRepo.Exists(id); + return PerformExists(id); + } + + public string GetUniqueAlias(string alias) + { + // alias is unique accross ALL content types! + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", + new { pattern = alias + "%", objectType = NodeObjectTypeId }); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) i++; + return test; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 95601011fa..b7b4ddd583 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -11,6 +14,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -27,46 +31,54 @@ namespace Umbraco.Core.Persistence.Repositories _templateRepository = templateRepository; } - #region Overrides of RepositoryBase - + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires:true)); + } + } + protected override IContentType PerformGet(int id) { - var contentTypes = ContentTypeQueryMapper.GetContentTypes( - new[] {id}, Database, SqlSyntax, this, _templateRepository); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); - } - else - { - var sql = new Sql().Select("id").From().Where(dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); + //NOTE: This logic should never be executed according to our cache policy + return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository) + .Where(x => ids.Contains(x.Id)); } + + return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, this, _templateRepository); } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.Text); + var sql = translator.Translate(); - var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) - : Enumerable.Empty(); + var dtos = Database.Fetch(sql); + + return + //This returns a lookup from the GetAll cached looup + (dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) + : Enumerable.Empty()) + //order the result by name + .OrderBy(x => x.Name); } - - #endregion - + /// /// Gets all entities of the specified query /// @@ -89,18 +101,39 @@ namespace Umbraco.Core.Persistence.Repositories return Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); } - #region Overrides of PetaPocoRepositoryBase + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + var sql = new Sql().Select("cmsContentType.alias") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId); + + if (objectTypes.Any()) + { + sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", objectTypes); + } + + return Database.Fetch(sql); + } protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .LeftJoin() - .On(left => left.ContentTypeNodeId, right => right.NodeId) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .LeftJoin(SqlSyntax) + .On(SqlSyntax, left => left.ContentTypeNodeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; @@ -135,11 +168,7 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.DocumentType); } } - - #endregion - - #region Unit of Work Implementation - + /// /// Deletes a content type /// @@ -158,6 +187,22 @@ namespace Umbraco.Core.Persistence.Repositories PersistDeletedItem((IEntity)child); } + //Before we call the base class methods to run all delete clauses, we need to first + // delete all of the property data associated with this document type. Normally this will + // be done in the ContentTypeService by deleting all associated content first, but in some cases + // like when we switch a document type, there is property data left over that is linked + // to the previous document type. So we need to ensure it's removed. + var sql = new Sql().Select("DISTINCT cmsPropertyData.propertytypeid") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.PropertyTypeId, dto => dto.Id) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, dto => dto.NodeId, dto => dto.ContentTypeId) + .Where(dto => dto.NodeId == entity.Id); + + //Delete all cmsPropertyData where propertytypeid EXISTS in the subquery above + Database.Execute(SqlSyntax.GetDeleteSubquery("cmsPropertyData", "propertytypeid", sql)); + base.PersistDeletedItem(entity); } @@ -178,26 +223,39 @@ namespace Umbraco.Core.Persistence.Repositories ((ContentType)entity).AddingEntity(); - var factory = new ContentTypeFactory(NodeObjectTypeId); - var dto = factory.BuildDto(entity); - - PersistNewBaseContentType(dto.ContentTypeDto, entity); - //Inserts data into the cmsDocumentType table if a template exists - if (dto.TemplateNodeId > 0) - { - dto.ContentTypeNodeId = entity.Id; - Database.Insert(dto); - } - - //Insert allowed Templates not including the default one, as that has already been inserted - foreach (var template in entity.AllowedTemplates.Where(x => x != null && x.Id != dto.TemplateNodeId)) - { - Database.Insert(new DocumentTypeDto { ContentTypeNodeId = entity.Id, TemplateNodeId = template.Id, IsDefault = false }); - } + PersistNewBaseContentType(entity); + PersistTemplates(entity, false); entity.ResetDirtyProperties(); } + protected void PersistTemplates(IContentType entity, bool clearAll) + { + // remove and insert, if required + Database.Delete("WHERE contentTypeNodeId = @Id", new { Id = entity.Id }); + + // we could do it all in foreach if we assume that the default template is an allowed template?? + var defaultTemplateId = ((ContentType) entity).DefaultTemplateId; + if (defaultTemplateId > 0) + { + Database.Insert(new ContentTypeTemplateDto + { + ContentTypeNodeId = entity.Id, + TemplateNodeId = defaultTemplateId, + IsDefault = true + }); + } + foreach (var template in entity.AllowedTemplates.Where(x => x != null && x.Id != defaultTemplateId)) + { + Database.Insert(new ContentTypeTemplateDto + { + ContentTypeNodeId = entity.Id, + TemplateNodeId = template.Id, + IsDefault = false + }); + } + } + protected override void PersistUpdatedItem(IContentType entity) { ValidateAlias(entity); @@ -218,51 +276,41 @@ namespace Umbraco.Core.Persistence.Repositories entity.SortOrder = maxSortOrder + 1; } - var factory = new ContentTypeFactory(NodeObjectTypeId); - var dto = factory.BuildDto(entity); - - PersistUpdatedBaseContentType(dto.ContentTypeDto, entity); - - //Look up DocumentType entries for updating - this could possibly be a "remove all, insert all"-approach - Database.Delete("WHERE contentTypeNodeId = @Id", new { Id = entity.Id }); - //Insert the updated DocumentTypeDto if a template exists - if (dto.TemplateNodeId > 0) - { - Database.Insert(dto); - } - - //Insert allowed Templates not including the default one, as that has already been inserted - foreach (var template in entity.AllowedTemplates.Where(x => x != null && x.Id != dto.TemplateNodeId)) - { - Database.Insert(new DocumentTypeDto { ContentTypeNodeId = entity.Id, TemplateNodeId = template.Id, IsDefault = false }); - } + PersistUpdatedBaseContentType(entity); + PersistTemplates(entity, true); entity.ResetDirtyProperties(); } - - #endregion - + protected override IContentType PerformGet(Guid id) { - var contentTypes = ContentTypeQueryMapper.GetContentTypes( - new[] { id }, Database, SqlSyntax, this, _templateRepository); + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); + } - var contentType = contentTypes.SingleOrDefault(); - return contentType; + protected override IContentType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } protected override IEnumerable PerformGetAll(params Guid[] ids) { + //use the underlying GetAll which will force cache all content types + if (ids.Any()) { - return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); + return GetAll().Where(x => ids.Contains(x.Key)); } else { - var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); + return GetAll(); } } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 859c4e7ae7..a4d71885f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -6,6 +6,8 @@ using System.Linq; using System.Text.RegularExpressions; using System.Threading; using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -24,17 +26,15 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DataTypeDefinitionRepository : PetaPocoRepositoryBase, IDataTypeDefinitionRepository { - private readonly CacheHelper _cacheHelper; private readonly IContentTypeRepository _contentTypeRepository; private readonly DataTypePreValueRepository _preValRepository; - public DataTypeDefinitionRepository(IDatabaseUnitOfWork work, CacheHelper cache, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax, + public DataTypeDefinitionRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentTypeRepository contentTypeRepository) : base(work, cache, logger, sqlSyntax) { - _cacheHelper = cacheHelper; _contentTypeRepository = contentTypeRepository; - _preValRepository = new DataTypePreValueRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); + _preValRepository = new DataTypePreValueRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); } #region Overrides of RepositoryBase @@ -116,9 +116,9 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = new Sql(); sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.DataTypeId, right => right.NodeId) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.DataTypeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } @@ -146,6 +146,10 @@ namespace Umbraco.Core.Persistence.Repositories { ((DataTypeDefinition)entity).AddingEntity(); + //ensure a datatype has a unique name before creating it + entity.Name = EnsureUniqueNodeName(entity.Name); + + //TODO: should the below be removed? //Cannot add a duplicate data type var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsDataType INNER JOIN umbracoNode ON cmsDataType.nodeId = umbracoNode.id @@ -191,6 +195,8 @@ WHERE umbracoNode." + SqlSyntax.GetQuotedColumnName("text") + "= @name", new { n protected override void PersistUpdatedItem(IDataTypeDefinition entity) { + entity.Name = EnsureUniqueNodeName(entity.Name, entity.Id); + //Cannot change to a duplicate alias var exists = Database.ExecuteScalar(@"SELECT COUNT(*) FROM cmsDataType INNER JOIN umbracoNode ON cmsDataType.nodeId = umbracoNode.id @@ -231,7 +237,7 @@ AND umbracoNode.id <> @id", //NOTE: This is a special case, we need to clear the custom cache for pre-values here so they are not stale if devs // are querying for them in the Saved event (before the distributed call cache is clearing it) - _cacheHelper.RuntimeCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); + RuntimeCache.ClearCacheItem(GetPrefixedCacheKey(entity.Id)); entity.ResetDirtyProperties(); } @@ -270,7 +276,7 @@ AND umbracoNode.id <> @id", public PreValueCollection GetPreValuesCollectionByDataTypeId(int dataTypeId) { - var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); + var cached = RuntimeCache.GetCacheItemsByKeySearch(GetPrefixedCacheKey(dataTypeId)); if (cached != null && cached.Any()) { //return from the cache, ensure it's a cloned result @@ -289,7 +295,7 @@ AND umbracoNode.id <> @id", { //We need to see if we can find the cached PreValueCollection based on the cache key above - var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); + var cached = RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); if (cached != null && cached.Any()) { //return from the cache @@ -323,6 +329,55 @@ AND umbracoNode.id <> @id", AddOrUpdatePreValues(dtd, values); } + public IEnumerable> Move(IDataTypeDefinition toMove, EntityContainer container) + { + var parentId = -1; + if (container != null) + { + // Check on paths + if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); + } + parentId = container.Id; + } + + //used to track all the moved entities to be given to the event + var moveInfo = new List> + { + new MoveEventInfo(toMove, toMove.Path, parentId) + }; + + var origPath = toMove.Path; + + //do the move to a new parent + toMove.ParentId = parentId; + + //set the updated path + toMove.Path = string.Concat(container == null ? parentId.ToInvariantString() : container.Path, ",", toMove.Id); + + //schedule it for updating in the transaction + AddOrUpdate(toMove); + + //update all descendants from the original path, update in order of level + var descendants = this.GetByQuery( + new Query().Where(type => type.Path.StartsWith(origPath + ","))); + + var lastParent = toMove; + foreach (var descendant in descendants.OrderBy(x => x.Level)) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + descendant.ParentId = lastParent.Id; + descendant.Path = string.Concat(lastParent.Path, ",", descendant.Id); + + //schedule it for updating in the transaction + AddOrUpdate(descendant); + } + + return moveInfo; + } + public void AddOrUpdatePreValues(IDataTypeDefinition dataType, IDictionary values) { var currentVals = new DataTypePreValueDto[] { }; @@ -330,9 +385,9 @@ AND umbracoNode.id <> @id", { //first just get all pre-values for this data type so we can compare them to see if we need to insert or update or replace var sql = new Sql().Select("*") - .From() + .From(SqlSyntax) .Where(dto => dto.DataTypeNodeId == dataType.Id) - .OrderBy(dto => dto.SortOrder); + .OrderBy(dto => dto.SortOrder, SqlSyntax); currentVals = Database.Fetch(sql).ToArray(); } @@ -408,7 +463,7 @@ AND umbracoNode.id <> @id", + string.Join(",", collection.FormatAsDictionary().Select(x => x.Value.Id).ToArray()); //store into cache - _cacheHelper.RuntimeCache.InsertCacheItem(key, () => collection, + RuntimeCache.InsertCacheItem(key, () => collection, //30 mins new TimeSpan(0, 0, 30), //sliding is true @@ -417,6 +472,37 @@ AND umbracoNode.id <> @id", return collection; } + private string EnsureUniqueNodeName(string nodeName, int id = 0) + { + + + var sql = new Sql(); + sql.Select("*") + .From(SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName)); + + int uniqueNumber = 1; + var currentName = nodeName; + + var dtos = Database.Fetch(sql); + if (dtos.Any()) + { + var results = dtos.OrderBy(x => x.Text, new SimilarNodeNameComparer()); + foreach (var dto in results) + { + if (id != 0 && id == dto.NodeId) continue; + + if (dto.Text.ToLowerInvariant().Equals(currentName.ToLowerInvariant())) + { + currentName = nodeName + string.Format(" ({0})", uniqueNumber); + uniqueNumber++; + } + } + } + + return currentName; + } + /// /// Private class to handle pre-value crud based on units of work with transactions /// @@ -527,6 +613,8 @@ AND umbracoNode.id <> @id", }; Database.Update(dto); } + + } internal static class PreValueConverter diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index 1985875717..971efc4f2d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -19,30 +20,40 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class DictionaryRepository : PetaPocoRepositoryBase, IDictionaryRepository { - private readonly ILanguageRepository _languageRepository; - - public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax, ILanguageRepository languageRepository) + public DictionaryRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider syntax) : base(work, cache, logger, syntax) - { - _languageRepository = languageRepository; - } + { + } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //custom cache policy which will not cache any results for GetAll + return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + })); + } + } #region Overrides of RepositoryBase protected override IDictionaryItem PerformGet(int id) { var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new {Id = id}) + .Where(GetBaseWhereClause(), new { Id = id }) .OrderBy(x => x.UniqueId, SqlSyntax); var dto = Database.Fetch(new DictionaryLanguageTextRelator().Map, sql).FirstOrDefault(); if (dto == null) return null; - - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - - var entity = ConvertFromDto(dto, allLanguages); + + var entity = ConvertFromDto(dto); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -56,14 +67,11 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false).Where("cmsDictionary.pk > 0"); if (ids.Any()) { - sql.Where("cmsDictionary.pk in (@ids)", new { ids = ids }); + sql.Where("cmsDictionary.pk in (@ids)", new { ids = ids }); } - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) - .Select(dto => ConvertFromDto(dto, allLanguages)); + .Select(dto => ConvertFromDto(dto)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -72,12 +80,9 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); sql.OrderBy(x => x.UniqueId, SqlSyntax); - - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - + return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) - .Select(x => ConvertFromDto(x, allLanguages)); + .Select(x => ConvertFromDto(x)); } #endregion @@ -87,7 +92,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); - if(isCount) + if (isCount) { sql.Select("COUNT(*)") .From(SqlSyntax); @@ -123,26 +128,28 @@ namespace Umbraco.Core.Persistence.Repositories protected override void PersistNewItem(IDictionaryItem entity) { - ((DictionaryItem)entity).AddingEntity(); + var dictionaryItem = ((DictionaryItem) entity); - foreach (var translation in entity.Translations) + dictionaryItem.AddingEntity(); + + foreach (var translation in dictionaryItem.Translations) translation.Value = translation.Value.ToValidXmlString(); var factory = new DictionaryItemFactory(); - var dto = factory.BuildDto(entity); + var dto = factory.BuildDto(dictionaryItem); var id = Convert.ToInt32(Database.Insert(dto)); - entity.Id = id; + dictionaryItem.Id = id; - var translationFactory = new DictionaryTranslationFactory(entity.Key, null); - foreach (var translation in entity.Translations) + var translationFactory = new DictionaryTranslationFactory(dictionaryItem.Key); + foreach (var translation in dictionaryItem.Translations) { var textDto = translationFactory.BuildDto(translation); translation.Id = Convert.ToInt32(Database.Insert(textDto)); - translation.Key = entity.Key; + translation.Key = dictionaryItem.Key; } - entity.ResetDirtyProperties(); + dictionaryItem.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IDictionaryItem entity) @@ -157,11 +164,11 @@ namespace Umbraco.Core.Persistence.Repositories Database.Update(dto); - var translationFactory = new DictionaryTranslationFactory(entity.Key, null); + var translationFactory = new DictionaryTranslationFactory(entity.Key); foreach (var translation in entity.Translations) { var textDto = translationFactory.BuildDto(translation); - if(translation.HasIdentity) + if (translation.HasIdentity) { Database.Update(textDto); } @@ -183,7 +190,7 @@ namespace Umbraco.Core.Persistence.Repositories { RecursiveDelete(entity.Key); - Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key}); + Database.Delete("WHERE UniqueId = @Id", new { Id = entity.Key }); Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key @@ -193,7 +200,7 @@ namespace Umbraco.Core.Persistence.Repositories private void RecursiveDelete(Guid parentId) { - var list = Database.Fetch("WHERE parent = @ParentId", new {ParentId = parentId}); + var list = Database.Fetch("WHERE parent = @ParentId", new { ParentId = parentId }); foreach (var dto in list) { RecursiveDelete(dto.UniqueId); @@ -209,20 +216,18 @@ namespace Umbraco.Core.Persistence.Repositories #endregion - protected IDictionaryItem ConvertFromDto(DictionaryDto dto, ILanguage[] allLanguages) + protected IDictionaryItem ConvertFromDto(DictionaryDto dto) { var factory = new DictionaryItemFactory(); var entity = factory.BuildEntity(dto); var list = new List(); foreach (var textDto in dto.LanguageTextDtos) - { - //Assuming this is cached! - var language = allLanguages.FirstOrDefault(x => x.Id == textDto.LanguageId); - if (language == null) + { + if (textDto.LanguageId <= 0) continue; - var translationFactory = new DictionaryTranslationFactory(dto.UniqueId, language); + var translationFactory = new DictionaryTranslationFactory(dto.UniqueId); list.Add(translationFactory.BuildEntity(textDto)); } entity.Translations = list; @@ -234,7 +239,7 @@ namespace Umbraco.Core.Persistence.Repositories { using (var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, UnitOfWork, RepositoryCache, Logger, SqlSyntax)) { - return uniqueIdRepo.Get(uniqueId); + return uniqueIdRepo.Get(uniqueId); } } @@ -242,10 +247,10 @@ namespace Umbraco.Core.Persistence.Repositories { using (var keyRepo = new DictionaryByKeyRepository(this, UnitOfWork, RepositoryCache, Logger, SqlSyntax)) { - return keyRepo.Get(key); + return keyRepo.Get(key); } } - + private IEnumerable GetRootDictionaryItems() { var query = Query.Builder.Where(x => x.ParentId == null); @@ -254,9 +259,6 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetDictionaryItemDescendants(Guid? parentId) { - //This will be cached - var allLanguages = _languageRepository.GetAll().ToArray(); - //This methods will look up children at each level, since we do not store a path for dictionary (ATM), we need to do a recursive // lookup to get descendants. Currently this is the most efficient way to do it @@ -275,7 +277,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.OrderBy(x => x.UniqueId, SqlSyntax); return Database.Fetch(new DictionaryLanguageTextRelator().Map, sql) - .Select(x => ConvertFromDto(x, allLanguages)); + .Select(x => ConvertFromDto(x)); }); }; @@ -284,7 +286,7 @@ namespace Umbraco.Core.Persistence.Repositories : getItemsFromParents(new[] { parentId.Value }); return childItems.SelectRecursive(items => getItemsFromParents(items.Select(x => x.Key).ToArray())).SelectMany(items => items); - + } private class DictionaryByUniqueIdRepository : SimpleGetRepository @@ -314,20 +316,34 @@ namespace Umbraco.Core.Persistence.Repositories protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) { - //This will be cached - var allLanguages = _dictionaryRepository._languageRepository.GetAll().ToArray(); - return _dictionaryRepository.ConvertFromDto(dto, allLanguages); + return _dictionaryRepository.ConvertFromDto(dto); } protected override object GetBaseWhereClauseArguments(Guid id) { - return new {Id = id}; + return new { Id = id }; } protected override string GetWhereInClauseForGetAll() { return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("id") + " in (@ids)"; } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //custom cache policy which will not cache any results for GetAll + return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + })); + } + } } private class DictionaryByKeyRepository : SimpleGetRepository @@ -357,9 +373,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override IDictionaryItem ConvertToEntity(DictionaryDto dto) { - //This will be cached - var allLanguages = _dictionaryRepository._languageRepository.GetAll().ToArray(); - return _dictionaryRepository.ConvertFromDto(dto, allLanguages); + return _dictionaryRepository.ConvertFromDto(dto); } protected override object GetBaseWhereClauseArguments(string id) @@ -371,17 +385,24 @@ namespace Umbraco.Core.Persistence.Repositories { return "cmsDictionary." + SqlSyntax.GetQuotedColumnName("key") + " in (@ids)"; } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //custom cache policy which will not cache any results for GetAll + return _cachePolicyFactory ?? (_cachePolicyFactory = new OnlySingleItemsRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions + { + //allow zero to be cached + GetAllCacheAllowZeroCount = true + })); + } + } } - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _languageRepository.Dispose(); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs index 21ba8b4baf..7b6cc162a8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DomainRepository.cs @@ -18,25 +18,20 @@ namespace Umbraco.Core.Persistence.Repositories internal class DomainRepository : PetaPocoRepositoryBase, IDomainRepository { - private readonly RepositoryCacheOptions _cacheOptions; - public DomainRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - //Custom cache options for better performance - _cacheOptions = new RepositoryCacheOptions - { - GetAllCacheAllowZeroCount = true, - GetAllCacheValidateCount = false - }; + { } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } } protected override IDomain PerformGet(int id) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs new file mode 100644 index 0000000000..49d52ffffa --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// An internal repository for managing entity containers such as doc type, media type, data type containers. + /// + internal class EntityContainerRepository : PetaPocoRepositoryBase + { + private readonly Guid _containerObjectType; + + public EntityContainerRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, Guid containerObjectType) + : base(work, cache, logger, sqlSyntax) + { + var allowedContainers = new[] {Constants.ObjectTypes.DocumentTypeContainerGuid, Constants.ObjectTypes.MediaTypeContainerGuid, Constants.ObjectTypes.DataTypeContainerGuid}; + _containerObjectType = containerObjectType; + if (allowedContainers.Contains(_containerObjectType) == false) + throw new InvalidOperationException("No container type exists with ID: " + _containerObjectType); + } + + /// + /// Do not cache anything + /// + protected override IRuntimeCacheProvider RuntimeCache + { + get { return new NullCacheProvider(); } + } + + protected override EntityContainer PerformGet(int id) + { + var sql = GetBaseQuery(false).Where(GetBaseWhereClause(), new { id = id, NodeObjectType = NodeObjectTypeId }); + + var nodeDto = Database.Fetch(sql).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } + + // temp - so we don't have to implement GetByQuery + public EntityContainer Get(Guid id) + { + var sql = GetBaseQuery(false).Where("UniqueId=@uniqueId", new { uniqueId = id }); + + var nodeDto = Database.Fetch(sql).FirstOrDefault(); + return nodeDto == null ? null : CreateEntity(nodeDto); + } + + public IEnumerable Get(string name, int level) + { + var sql = GetBaseQuery(false).Where("text=@name AND level=@level AND nodeObjectType=@umbracoObjectTypeId", new { name, level, umbracoObjectTypeId = NodeObjectTypeId }); + return Database.Fetch(sql).Select(CreateEntity); + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + return ids.InGroupsOf(2000).SelectMany(@group => + { + var sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) + .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new { ids = @group }); + + sql.OrderBy(x => x.Level, SqlSyntax); + + return Database.Fetch(sql).Select(CreateEntity); + }); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + + private static EntityContainer CreateEntity(NodeDto nodeDto) + { + if (nodeDto.NodeObjectType.HasValue == false) + throw new InvalidOperationException("Node with id " + nodeDto.NodeId + " has no object type."); + + // throws if node is not a container + var containedObjectType = EntityContainer.GetContainedObjectType(nodeDto.NodeObjectType.Value); + + var entity = new EntityContainer(nodeDto.NodeId, nodeDto.UniqueId, + nodeDto.ParentId, nodeDto.Path, nodeDto.Level, nodeDto.SortOrder, + containedObjectType, + nodeDto.Text, nodeDto.UserId ?? 0); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + + return entity; + } + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + if (isCount) + { + sql.Select("COUNT(*)").From(SqlSyntax); + } + else + { + sql.Select("*").From(SqlSyntax); + } + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @id and nodeObjectType = @NodeObjectType"; + } + + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + + protected override Guid NodeObjectTypeId + { + get { return _containerObjectType; } + } + + protected override void PersistDeletedItem(EntityContainer entity) + { + EnsureContainerType(entity); + + var nodeDto = Database.FirstOrDefault(new Sql().Select("*") + .From(SqlSyntax) + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + + if (nodeDto == null) return; + + // move children to the parent so they are not orphans + var childDtos = Database.Fetch(new Sql().Select("*") + .From(SqlSyntax) + .Where("parentID=@parentID AND (nodeObjectType=@containedObjectType OR nodeObjectType=@containerObjectType)", + new + { + parentID = entity.Id, + containedObjectType = entity.ContainedObjectType, + containerObjectType = entity.ContainerObjectType + })); + + foreach (var childDto in childDtos) + { + childDto.ParentId = nodeDto.ParentId; + Database.Update(childDto); + } + + // delete + Database.Delete(nodeDto); + } + + protected override void PersistNewItem(EntityContainer entity) + { + EnsureContainerType(entity); + + entity.Name = entity.Name.Trim(); + Mandate.ParameterNotNullOrEmpty(entity.Name, "entity.Name"); + + // guard against duplicates + var nodeDto = Database.FirstOrDefault(new Sql().Select("*") + .From(SqlSyntax) + .Where(dto => dto.ParentId == entity.ParentId && dto.Text == entity.Name && dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto != null) + throw new InvalidOperationException("A container with the same name already exists."); + + // create + var level = 0; + var path = "-1"; + if (entity.ParentId > -1) + { + var parentDto = Database.FirstOrDefault(new Sql().Select("*") + .From(SqlSyntax) + .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + + if (parentDto == null) + throw new NullReferenceException("Could not find parent container with id " + entity.ParentId); + + level = parentDto.Level; + path = parentDto.Path; + } + + // note: sortOrder is NOT managed and always zero for containers + + nodeDto = new NodeDto + { + CreateDate = DateTime.Now, + Level = Convert.ToInt16(level + 1), + NodeObjectType = entity.ContainerObjectType, + ParentId = entity.ParentId, + Path = path, + SortOrder = 0, + Text = entity.Name, + UserId = entity.CreatorId, + UniqueId = entity.Key + }; + + // insert, get the id, update the path with the id + var id = Convert.ToInt32(Database.Insert(nodeDto)); + nodeDto.Path = nodeDto.Path + "," + nodeDto.NodeId; + Database.Save(nodeDto); + + // refresh the entity + entity.Id = id; + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.CreateDate = nodeDto.CreateDate; + entity.ResetDirtyProperties(); + } + + // beware! does NOT manage descendants in case of a new parent + // + protected override void PersistUpdatedItem(EntityContainer entity) + { + EnsureContainerType(entity); + + entity.Name = entity.Name.Trim(); + Mandate.ParameterNotNullOrEmpty(entity.Name, "entity.Name"); + + // find container to update + var nodeDto = Database.FirstOrDefault(new Sql().Select("*") + .From(SqlSyntax) + .Where(dto => dto.NodeId == entity.Id && dto.NodeObjectType == entity.ContainerObjectType)); + if (nodeDto == null) + throw new InvalidOperationException("Could not find container with id " + entity.Id); + + // guard against duplicates + var dupNodeDto = Database.FirstOrDefault(new Sql().Select("*") + .From(SqlSyntax) + .Where(dto => dto.ParentId == entity.ParentId && dto.Text == entity.Name && dto.NodeObjectType == entity.ContainerObjectType)); + if (dupNodeDto != null && dupNodeDto.NodeId != nodeDto.NodeId) + throw new InvalidOperationException("A container with the same name already exists."); + + // update + nodeDto.Text = entity.Name; + if (nodeDto.ParentId != entity.ParentId) + { + nodeDto.Level = 0; + nodeDto.Path = "-1"; + if (entity.ParentId > -1) + { + var parent = Database.FirstOrDefault(new Sql().Select("*") + .From(SqlSyntax) + .Where(dto => dto.NodeId == entity.ParentId && dto.NodeObjectType == entity.ContainerObjectType)); + + if (parent == null) + throw new NullReferenceException("Could not find parent container with id " + entity.ParentId); + + nodeDto.Level = Convert.ToInt16(parent.Level + 1); + nodeDto.Path = parent.Path + "," + nodeDto.NodeId; + } + nodeDto.ParentId = entity.ParentId; + } + + // note: sortOrder is NOT managed and always zero for containers + + // update + Database.Update(nodeDto); + + // refresh the entity + entity.Path = nodeDto.Path; + entity.Level = nodeDto.Level; + entity.SortOrder = 0; + entity.ResetDirtyProperties(); + } + + private void EnsureContainerType(EntityContainer entity) + { + if (entity.ContainerObjectType != NodeObjectTypeId) + { + throw new InvalidOperationException("The container type does not match the repository object type"); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs index 97c6e4fcf5..276f4b0f89 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -67,7 +67,7 @@ namespace Umbraco.Core.Persistence.Repositories //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + entity.ResetDirtyProperties(false); return entity; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index 9d3fcbb40b..ab176df6d0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -84,9 +84,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs new file mode 100644 index 0000000000..649726b100 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeCompositionRepository.cs @@ -0,0 +1,11 @@ +using System; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IContentTypeCompositionRepository : IRepositoryQueryable, IReadRepository + where TEntity : IContentTypeComposition + { + TEntity Get(string alias); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index ca7e0e3c32..61d83645b3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentTypeRepository : IRepositoryQueryable, IReadRepository + public interface IContentTypeRepository : IContentTypeCompositionRepository { /// /// Gets all entities of the specified query @@ -19,5 +20,25 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetAllPropertyTypeAliases(); + + IEnumerable> Move(IContentType toMove, EntityContainer container); + + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// /// Unique accross all content, media and member types. + string GetUniqueAlias(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDataTypeDefinitionRepository.cs index f04b90c2e0..6d7265f717 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDataTypeDefinitionRepository.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.UnitOfWork; @@ -11,5 +12,6 @@ namespace Umbraco.Core.Persistence.Repositories void AddOrUpdatePreValues(IDataTypeDefinition dataType, IDictionary values); void AddOrUpdatePreValues(int dataTypeId, IDictionary values); + IEnumerable> Move(IDataTypeDefinition toMove, EntityContainer container); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 46bce6a03c..907f9b62c5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.Repositories { public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { - + /// /// Used to add/update published xml for the media item /// @@ -33,9 +33,10 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs index d28c59ac5b..7f2f76e541 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaTypeRepository : IRepositoryQueryable, IReadRepository + public interface IMediaTypeRepository : IContentTypeCompositionRepository { /// /// Gets all entities of the specified query @@ -13,5 +13,15 @@ namespace Umbraco.Core.Persistence.Repositories /// /// An enumerable list of objects IEnumerable GetByQuery(IQuery query); + + IEnumerable> Move(IMediaType toMove, EntityContainer container); + + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + /// Unique accross all content, media and member types. + string GetUniqueAlias(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 9cb74d1806..a24116f0e2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -44,16 +44,17 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Gets paged member results /// - /// - /// - /// - /// - /// - /// - /// + /// The query. + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query /// IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = ""); + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); //IEnumerable GetPagedResultsByQuery( // Sql sql, int pageIndex, int pageSize, out int totalRecords, diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs index ae7739b28b..fc877d5227 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs @@ -3,7 +3,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberTypeRepository : IRepositoryQueryable, IReadRepository + public interface IMemberTypeRepository : IContentTypeCompositionRepository { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs index 14957ac49f..3faba9e282 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories @@ -22,6 +23,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// [Obsolete("Use GetDescendants instead")] + [EditorBrowsable(EditorBrowsableState.Never)] TemplateNode GetTemplateNode(string alias); /// @@ -31,6 +33,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// [Obsolete("Use GetDescendants instead")] + [EditorBrowsable(EditorBrowsableState.Never)] TemplateNode FindTemplateInTree(TemplateNode anyNode, string alias); /// diff --git a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs index 2f379b4587..f9a8e59cfa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/LanguageRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -20,23 +21,18 @@ namespace Umbraco.Core.Persistence.Repositories { public LanguageRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - //Custom cache options for better performance - _cacheOptions = new RepositoryCacheOptions - { - GetAllCacheAllowZeroCount = true, - GetAllCacheValidateCount = false - }; + { } - private readonly RepositoryCacheOptions _cacheOptions; - - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } } #region Overrides of RepositoryBase @@ -159,13 +155,13 @@ namespace Umbraco.Core.Persistence.Repositories public ILanguage GetByCultureName(string cultureName) { - //use the underlying GetAll which will force cache all domains + //use the underlying GetAll which will force cache all languages return GetAll().FirstOrDefault(x => x.CultureName.InvariantEquals(cultureName)); } public ILanguage GetByIsoCode(string isoCode) { - //use the underlying GetAll which will force cache all domains + //use the underlying GetAll which will force cache all languages return GetAll().FirstOrDefault(x => x.IsoCode.InvariantEquals(isoCode)); } diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 162ad88ca0..54a8b4409f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -232,7 +232,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -442,10 +442,11 @@ namespace Umbraco.Core.Persistence.Repositories /// Total records query would return without paging /// Field to order by /// Direction to order by + /// Flag to indicate when ordering by system field /// Search text filter /// An Enumerable list of objects public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { var args = new List(); var sbWhere = new StringBuilder(); @@ -459,7 +460,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -468,7 +469,7 @@ namespace Umbraco.Core.Persistence.Repositories { //NOTE: This doesn't allow properties to be part of the query var dtos = Database.Fetch(sql); - + var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); //content types diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index bf963492d2..50a89bfd65 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -10,6 +13,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -24,47 +28,54 @@ namespace Umbraco.Core.Persistence.Repositories { } - #region Overrides of RepositoryBase + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); + } + } protected override IMediaType PerformGet(int id) { - var contentTypes = ContentTypeQueryMapper.GetMediaTypes( - new[] { id }, Database, SqlSyntax, this); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) { if (ids.Any()) { - return ContentTypeQueryMapper.GetMediaTypes(ids, Database, SqlSyntax, this); - } - else - { - var sql = new Sql().Select("id").From().Where(dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetMediaTypes(allIds, Database, SqlSyntax, this); + //NOTE: This logic should never be executed according to our cache policy + return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this) + .Where(x => ids.Contains(x.Id)); } + + return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, this); } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .OrderBy(x => x.Text); + var sql = translator.Translate(); var dtos = Database.Fetch(sql); - return dtos.Any() - ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) - : Enumerable.Empty(); + + return + //This returns a lookup from the GetAll cached looup + (dtos.Any() + ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) + : Enumerable.Empty()) + //order the result by name + .OrderBy(x => x.Name); } - - #endregion - - + /// /// Gets all entities of the specified query /// @@ -76,17 +87,15 @@ namespace Umbraco.Core.Persistence.Repositories return ints.Any() ? GetAll(ints) : Enumerable.Empty(); - } - - #region Overrides of PetaPocoRepositoryBase - + } + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } @@ -119,19 +128,12 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.MediaType); } } - - #endregion - - #region Unit of Work Implementation - + protected override void PersistNewItem(IMediaType entity) { ((MediaType)entity).AddingEntity(); - var factory = new MediaTypeFactory(NodeObjectTypeId); - var dto = factory.BuildDto(entity); - - PersistNewBaseContentType(dto, entity); + PersistNewBaseContentType(entity); entity.ResetDirtyProperties(); } @@ -156,37 +158,40 @@ namespace Umbraco.Core.Persistence.Repositories entity.SortOrder = maxSortOrder + 1; } - var factory = new MediaTypeFactory(NodeObjectTypeId); - var dto = factory.BuildDto(entity); - - PersistUpdatedBaseContentType(dto, entity); + PersistUpdatedBaseContentType(entity); entity.ResetDirtyProperties(); } - #endregion - protected override IMediaType PerformGet(Guid id) { - var contentTypes = ContentTypeQueryMapper.GetMediaTypes( - new[] { id }, Database, SqlSyntax, this); - - var contentType = contentTypes.SingleOrDefault(); - return contentType; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); } protected override IEnumerable PerformGetAll(params Guid[] ids) { + //use the underlying GetAll which will force cache all content types + if (ids.Any()) { - return ContentTypeQueryMapper.GetMediaTypes(ids, Database, SqlSyntax, this); + return GetAll().Where(x => ids.Contains(x.Key)); } else { - var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); - var allIds = Database.Fetch(sql).ToArray(); - return ContentTypeQueryMapper.GetMediaTypes(allIds, Database, SqlSyntax, this); + return GetAll(); } } + + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } + + protected override IMediaType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs index 264409ddf4..40121223a2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberGroupRepository.cs @@ -20,13 +20,9 @@ namespace Umbraco.Core.Persistence.Repositories internal class MemberGroupRepository : PetaPocoRepositoryBase, IMemberGroupRepository { - private readonly CacheHelper _cacheHelper; - - public MemberGroupRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, CacheHelper cacheHelper) + public MemberGroupRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { - if (cacheHelper == null) throw new ArgumentNullException("cacheHelper"); - _cacheHelper = cacheHelper; } private readonly MemberGroupFactory _modelFactory = new MemberGroupFactory(); @@ -135,7 +131,7 @@ namespace Umbraco.Core.Persistence.Repositories public IMemberGroup GetByName(string name) { - return _cacheHelper.RuntimeCache.GetCacheItem( + return RuntimeCache.GetCacheItem( string.Format("{0}.{1}", typeof (IMemberGroup).FullName, name), () => { diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index ac5529b523..dfbcf61b7f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -74,7 +74,7 @@ namespace Umbraco.Core.Persistence.Repositories } return ProcessQuery(sql); - + } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -95,7 +95,7 @@ namespace Umbraco.Core.Persistence.Repositories baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) .OrderBy(x => x.SortOrder); - return ProcessQuery(baseQuery); + return ProcessQuery(baseQuery); } else { @@ -103,7 +103,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql); } } @@ -118,7 +118,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Select(isCount ? "COUNT(*)" : "*") .From() .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + .On(left => left.NodeId, right => right.NodeId) .InnerJoin() .On(left => left.NodeId, right => right.NodeId) //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? @@ -269,7 +269,7 @@ namespace Umbraco.Core.Persistence.Repositories //Ensure that strings don't contain characters that are invalid in XML entity.SanitizeEntityPropertiesForXmlStorage(); - var dirtyEntity = (ICanBeDirty) entity; + var dirtyEntity = (ICanBeDirty)entity; //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed if (dirtyEntity.IsPropertyDirty("ParentId")) @@ -308,9 +308,9 @@ namespace Umbraco.Core.Persistence.Repositories //Updates the current version - cmsContentVersion //Assumes a Version guid exists and Version date (modified date) has been set/updated Database.Update(dto.ContentVersionDto); - + //Updates the cmsMember entry if it has changed - + //NOTE: these cols are the REAL column names in the db var changedCols = new List(); @@ -330,13 +330,13 @@ namespace Umbraco.Core.Persistence.Repositories //only update the changed cols if (changedCols.Count > 0) { - Database.Update(dto, changedCols); + Database.Update(dto, changedCols); } //TODO ContentType for the Member entity //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); var keyDictionary = new Dictionary(); //Add Properties @@ -447,7 +447,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -559,7 +559,7 @@ namespace Umbraco.Core.Persistence.Repositories var grpQry = new Query().Where(group => group.Name.Equals(groupName)); var memberGroup = _memberGroupRepository.GetByQuery(grpQry).FirstOrDefault(); if (memberGroup == null) return Enumerable.Empty(); - + var subQuery = new Sql().Select("Member").From().Where(dto => dto.MemberGroup == memberGroup.Id); var sql = GetBaseQuery(false) @@ -568,7 +568,7 @@ namespace Umbraco.Core.Persistence.Repositories .Append(new Sql("WHERE umbracoNode.id IN (" + subQuery.SQL + ")", subQuery.Arguments)) .OrderByDescending(x => x.VersionDate) .OrderBy(x => x.SortOrder); - + return ProcessQuery(sql); } @@ -593,7 +593,7 @@ namespace Umbraco.Core.Persistence.Repositories //get the COUNT base query var fullSql = GetBaseQuery(true) .Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)); - + return Database.ExecuteScalar(fullSql); } @@ -603,18 +603,19 @@ namespace Umbraco.Core.Persistence.Repositories /// /// The where clause, if this is null all records are queried /// - /// - /// - /// - /// - /// - /// + /// Index of the page. + /// Size of the page. + /// The total records. + /// The order by column + /// The order direction. + /// Flag to indicate when ordering by system field + /// Search query /// /// /// The query supplied will ONLY work with data specifically on the cmsMember table because we are using PetaPoco paging (SQL paging) /// public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, string filter = "") + string orderBy, Direction orderDirection, bool orderBySystemField, string filter = "") { var args = new List(); var sbWhere = new StringBuilder(); @@ -625,14 +626,14 @@ namespace Umbraco.Core.Persistence.Repositories "OR (cmsMember.LoginName LIKE @0" + args.Count + "))"); args.Add("%" + filter + "%"); filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); - } + } return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - ProcessQuery, orderBy, orderDirection, + ProcessQuery, orderBy, orderDirection, orderBySystemField, filterCallback); } - + public void AddOrUpdateContentXml(IMember content, Func xml) { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); @@ -682,7 +683,7 @@ namespace Umbraco.Core.Persistence.Repositories var dtosWithContentTypes = dtos //This select into and null check are required because we don't have a foreign damn key on the contentType column // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new {dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId)}) + .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) .Where(x => x.contentType != null) .ToArray(); @@ -694,12 +695,12 @@ namespace Umbraco.Core.Persistence.Repositories d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, d.contentType)); - var propertyData = GetPropertyCollection(sql, docDefs); + var propertyData = GetPropertyCollection(sql, docDefs); return dtosWithContentTypes.Select(d => CreateMemberFromDto( d.dto, contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); + propertyData[d.dto.NodeId])); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index 39f2a93082..d06399a85b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using log4net; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; @@ -26,25 +27,23 @@ namespace Umbraco.Core.Persistence.Repositories { } - #region Overrides of RepositoryBase - + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), + //allow this cache to expire + expires: true)); + } + } + protected override IMemberType PerformGet(int id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - sql.OrderByDescending(x => x.NodeId); - - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); - - if (dtos == null || dtos.Any() == false) - return null; - - var factory = new MemberTypeReadOnlyFactory(); - var member = factory.BuildEntity(dtos.First()); - - return member; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -52,10 +51,11 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false); if (ids.Any()) { + //NOTE: This logic should never be executed according to our cache policy var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.id='{0}'", x))); sql.Where(statement); } - sql.OrderByDescending(x => x.NodeId); + sql.OrderByDescending(x => x.NodeId, SqlSyntax); var dtos = Database.Fetch( @@ -71,7 +71,7 @@ namespace Umbraco.Core.Persistence.Repositories var subquery = translator.Translate(); var sql = GetBaseQuery(false) .Append(new Sql("WHERE umbracoNode.id IN (" + subquery.SQL + ")", subquery.Arguments)) - .OrderBy(x => x.SortOrder); + .OrderBy(x => x.SortOrder, SqlSyntax); var dtos = Database.Fetch( @@ -79,11 +79,7 @@ namespace Umbraco.Core.Persistence.Repositories return BuildFromDtos(dtos); } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -98,11 +94,11 @@ namespace Umbraco.Core.Persistence.Repositories } sql.Select("umbracoNode.*", "cmsContentType.*", "cmsPropertyType.id AS PropertyTypeId", "cmsPropertyType.Alias", - "cmsPropertyType.Name", "cmsPropertyType.Description", "cmsPropertyType.mandatory", + "cmsPropertyType.Name", "cmsPropertyType.Description", "cmsPropertyType.mandatory", "cmsPropertyType.UniqueID", "cmsPropertyType.validationRegExp", "cmsPropertyType.dataTypeId", "cmsPropertyType.sortOrder AS PropertyTypeSortOrder", "cmsPropertyType.propertyTypeGroupId AS PropertyTypesGroupId", "cmsMemberType.memberCanEdit", "cmsMemberType.viewOnProfile", "cmsDataType.propertyEditorAlias", "cmsDataType.dbType", "cmsPropertyTypeGroup.id AS PropertyTypeGroupId", - "cmsPropertyTypeGroup.text AS PropertyGroupName", "cmsPropertyTypeGroup.parentGroupId", + "cmsPropertyTypeGroup.text AS PropertyGroupName", "cmsPropertyTypeGroup.uniqueID AS PropertyGroupUniqueID", "cmsPropertyTypeGroup.sortorder AS PropertyGroupSortOrder", "cmsPropertyTypeGroup.contenttypeNodeId") .From() .InnerJoin().On(left => left.NodeId, right => right.NodeId) @@ -157,11 +153,7 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.MemberType); } } - - #endregion - - #region Unit of Work Implementation - + protected override void PersistNewItem(IMemberType entity) { ValidateAlias(entity); @@ -182,12 +174,10 @@ namespace Umbraco.Core.Persistence.Repositories entity.AddPropertyType(standardPropertyType.Value, Constants.Conventions.Member.StandardPropertiesGroupName); } - var factory = new MemberTypeFactory(NodeObjectTypeId); - var dto = factory.BuildDto(entity); + var factory = new ContentTypeFactory(); EnsureExplicitDataTypeForBuiltInProperties(entity); - - PersistNewBaseContentType(dto, entity); + PersistNewBaseContentType(entity); //Handles the MemberTypeDto (cmsMemberType table) var memberTypeDtos = factory.BuildMemberTypeDtos(entity); @@ -219,17 +209,13 @@ namespace Umbraco.Core.Persistence.Repositories entity.SortOrder = maxSortOrder + 1; } - var factory = new MemberTypeFactory(NodeObjectTypeId); - var dto = factory.BuildDto(entity); + var factory = new ContentTypeFactory(); EnsureExplicitDataTypeForBuiltInProperties(entity); + PersistUpdatedBaseContentType(entity); - PersistUpdatedBaseContentType(dto, entity); - - //Remove existing entries before inserting new ones + // remove and insert - handle cmsMemberType table Database.Delete("WHERE NodeId = @Id", new { Id = entity.Id }); - - //Handles the MemberTypeDto (cmsMemberType table) var memberTypeDtos = factory.BuildMemberTypeDtos(entity); foreach (var memberTypeDto in memberTypeDtos) { @@ -238,9 +224,7 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - #endregion - + /// /// Override so we can specify explicit db type's on any property types that are built-in. /// @@ -262,38 +246,33 @@ namespace Umbraco.Core.Persistence.Repositories protected override IMemberType PerformGet(Guid id) { - var sql = GetBaseQuery(false); - sql.Where("umbracoNode.uniqueID = @Id", new { Id = id }); - sql.OrderByDescending(x => x.NodeId); - - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); - - if (dtos == null || dtos.Any() == false) - return null; - - var factory = new MemberTypeReadOnlyFactory(); - var member = factory.BuildEntity(dtos.First()); - - return member; + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Key == id); } protected override IEnumerable PerformGetAll(params Guid[] ids) { - var sql = GetBaseQuery(false); + //use the underlying GetAll which will force cache all content types + if (ids.Any()) { - var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.uniqueID='{0}'", x))); - sql.Where(statement); + return GetAll().Where(x => ids.Contains(x.Key)); } - sql.OrderByDescending(x => x.NodeId, SqlSyntax); + else + { + return GetAll(); + } + } - var dtos = - Database.Fetch( - new PropertyTypePropertyGroupRelator().Map, sql); + protected override bool PerformExists(Guid id) + { + return GetAll().FirstOrDefault(x => x.Key == id) != null; + } - return BuildFromDtos(dtos); + protected override IMemberType PerformGet(string alias) + { + //use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs index 0463312982..aa671dccec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PermissionRepository.cs @@ -26,13 +26,14 @@ namespace Umbraco.Core.Persistence.Repositories where TEntity : class, IAggregateRoot { private readonly IDatabaseUnitOfWork _unitOfWork; - private readonly CacheHelper _cache; + private readonly IRuntimeCacheProvider _runtimeCache; private readonly ISqlSyntaxProvider _sqlSyntax; internal PermissionRepository(IDatabaseUnitOfWork unitOfWork, CacheHelper cache, ISqlSyntaxProvider sqlSyntax) { _unitOfWork = unitOfWork; - _cache = cache; + //Make this repository use an isolated cache + _runtimeCache = cache.IsolatedRuntimeCache.GetOrCreateCache(); _sqlSyntax = sqlSyntax; } @@ -45,7 +46,7 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetUserPermissionsForEntities(int userId, params int[] entityIds) { var entityIdKey = string.Join(",", entityIds.Select(x => x.ToString(CultureInfo.InvariantCulture))); - return _cache.RuntimeCache.GetCacheItem>( + return _runtimeCache.GetCacheItem>( string.Format("{0}{1}{2}", CacheKeys.UserPermissionsCacheKey, userId, entityIdKey), () => { diff --git a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs index 295ff8b408..22fad9d99b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/PublicAccessRepository.cs @@ -17,33 +17,24 @@ namespace Umbraco.Core.Persistence.Repositories { public PublicAccessRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) - { - _options = new RepositoryCacheOptions - { - //We want to ensure that a zero count gets cached, even if there is nothing in the db we don't want it to lookup nothing each time - GetAllCacheAllowZeroCount = true, - //Set to 1000 just to ensure that all of them are cached, The GetAll on this repository gets called *A lot*, we want max performance - GetAllCacheThresholdLimit = 1000, - //Override to false so that a Count check against the db is NOT performed when doing a GetAll without params, we just want to - // return the raw cache without validation. The GetAll on this repository gets called *A lot*, we want max performance - GetAllCacheValidateCount = false - }; + { } - private readonly RepositoryCacheOptions _options; + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } + } protected override PublicAccessEntry PerformGet(Guid id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - - var taskDto = Database.Fetch(new AccessRulesRelator().Map, sql).FirstOrDefault(); - if (taskDto == null) - return null; - - var factory = new PublicAccessEntryFactory(); - var entity = factory.BuildEntity(taskDto); - return entity; + //return from GetAll - this will be cached as a collection + return GetAll().FirstOrDefault(x => x.Key == id); } protected override IEnumerable PerformGetAll(params Guid[] ids) @@ -102,15 +93,6 @@ namespace Umbraco.Core.Persistence.Repositories get { throw new NotImplementedException(); } } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions - { - get { return _options; } - } - - protected override void PersistNewItem(PublicAccessEntry entity) { entity.AddingEntity(); diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index f9088e1911..5534a9ea40 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Cache; +using Umbraco.Core.Collections; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; @@ -22,11 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories if (logger == null) throw new ArgumentNullException("logger"); Logger = logger; _work = work; - - //IMPORTANT: We will force the DeepCloneRuntimeCacheProvider to be used here which is a wrapper for the underlying - // runtime cache to ensure that anything that can be deep cloned in/out is done so, this also ensures that our tracks - // changes entities are reset. - _cache = new CacheHelper(new DeepCloneRuntimeCacheProvider(cache.RuntimeCache), cache.StaticCache, cache.RequestCache); + _cache = cache; } /// @@ -84,9 +82,41 @@ namespace Umbraco.Core.Persistence.Repositories { } - private readonly RepositoryCacheOptions _cacheOptions = new RepositoryCacheOptions(); - #region IRepository Members + protected virtual TId GetEntityId(TEntity entity) + { + return (TId)(object)entity.Id; + } + + /// + /// The runtime cache used for this repo by default is the isolated cache for this type + /// + protected override IRuntimeCacheProvider RuntimeCache + { + get { return RepositoryCache.IsolatedRuntimeCache.GetOrCreateCache(); } + } + + private IRepositoryCachePolicyFactory _cachePolicyFactory; + /// + /// Returns the Cache Policy for the repository + /// + /// + /// The Cache Policy determines how each entity or entity collection is cached + /// + protected virtual IRepositoryCachePolicyFactory CachePolicyFactory + { + get + { + return _cachePolicyFactory ?? (_cachePolicyFactory = new DefaultRepositoryCachePolicyFactory( + RuntimeCache, + new RepositoryCachePolicyOptions(() => + { + //Get count of all entities of current type (TEntity) to ensure cached result is correct + var query = Query.Builder.Where(x => x.Id != 0); + return PerformCount(query); + }))); + } + } /// /// Adds or Updates an entity of type TEntity @@ -119,23 +149,16 @@ namespace Umbraco.Core.Persistence.Repositories protected abstract TEntity PerformGet(TId id); /// - /// Gets an entity by the passed in Id utilizing the repository's runtime cache + /// Gets an entity by the passed in Id utilizing the repository's cache policy /// /// /// public TEntity Get(TId id) { - var cacheKey = GetCacheIdKey(id); - var fromCache = RuntimeCache.GetCacheItem(cacheKey); - - if (fromCache != null) return fromCache; - - var entity = PerformGet(id); - if (entity == null) return null; - - RuntimeCache.InsertCacheItem(cacheKey, () => entity); - - return entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + return p.Get(id, PerformGet); + } } protected abstract IEnumerable PerformGetAll(params TId[] ids); @@ -158,88 +181,13 @@ namespace Umbraco.Core.Persistence.Repositories throw new InvalidOperationException("Cannot perform a query with more than 2000 parameters"); } - if (ids.Any()) + using (var p = CachePolicyFactory.CreatePolicy()) { - var entities = ids.Select(x => RuntimeCache.GetCacheItem(GetCacheIdKey(x))).ToArray(); - - if (ids.Count().Equals(entities.Count()) && entities.Any(x => x == null) == false) - return entities; - } - else - { - var allEntities = RuntimeCache.GetCacheItemsByKeySearch(GetCacheTypeKey()) - .WhereNotNull() - .ToArray(); - - if (allEntities.Any()) - { - - if (RepositoryCacheOptions.GetAllCacheValidateCount) - { - //Get count of all entities of current type (TEntity) to ensure cached result is correct - var query = Query.Builder.Where(x => x.Id != 0); - int totalCount = PerformCount(query); - - if (allEntities.Count() == totalCount) - return allEntities; - } - else - { - return allEntities; - } - } - else if (RepositoryCacheOptions.GetAllCacheAllowZeroCount) - { - //if the repository allows caching a zero count, then check the zero count cache - var zeroCount = RuntimeCache.GetCacheItem(GetCacheTypeKey()); - if (zeroCount != null && zeroCount.Any() == false) - { - //there is a zero count cache so return an empty list - return Enumerable.Empty(); - } - } - - } - - var entityCollection = PerformGetAll(ids) - //ensure we don't include any null refs in the returned collection! - .WhereNotNull() - .ToArray(); - - //We need to put a threshold here! IF there's an insane amount of items - // coming back here we don't want to chuck it all into memory, this added cache here - // is more for convenience when paging stuff temporarily - - if (entityCollection.Length > RepositoryCacheOptions.GetAllCacheThresholdLimit) - return entityCollection; - - if (entityCollection.Length == 0 && RepositoryCacheOptions.GetAllCacheAllowZeroCount) - { - //there was nothing returned but we want to cache a zero count result so add an TEntity[] to the cache - // to signify that there is a zero count cache - RuntimeCache.InsertCacheItem(GetCacheTypeKey(), () => new TEntity[] {}); - } - - foreach (var entity in entityCollection) - { - if (entity != null) - { - var localCopy = entity; - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => localCopy); - } - } - - return entityCollection; + var result = p.GetAll(ids, PerformGetAll); + return result; + } } - - /// - /// Returns the repository cache options - /// - protected virtual RepositoryCacheOptions RepositoryCacheOptions - { - get { return _cacheOptions; } - } - + protected abstract IEnumerable PerformGetByQuery(IQuery query); /// /// Gets a list of entities by the passed in query @@ -261,12 +209,10 @@ namespace Umbraco.Core.Persistence.Repositories /// public bool Exists(TId id) { - var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(id)); - if (fromCache != null) + using (var p = CachePolicyFactory.CreatePolicy()) { - return true; + return p.Exists(id, PerformExists); } - return PerformExists(id); } protected abstract int PerformCount(IQuery query); @@ -279,34 +225,19 @@ namespace Umbraco.Core.Persistence.Repositories { return PerformCount(query); } - - #endregion - - #region IUnitOfWorkRepository Members - + /// /// Unit of work method that tells the repository to persist the new entity /// /// public virtual void PersistNewItem(IEntity entity) { - try - { - PersistNewItem((TEntity)entity); - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - } - catch (Exception) - { - //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - throw; - } + var casted = (TEntity)entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.CreateOrUpdate(casted, PersistNewItem); + } } /// @@ -315,23 +246,12 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistUpdatedItem(IEntity entity) { - try - { - PersistUpdatedItem((TEntity)entity); - RuntimeCache.InsertCacheItem(GetCacheIdKey(entity.Id), () => entity); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - } - catch (Exception) - { - //if an exception is thrown we need to remove the entry from cache, this is ONLY a work around because of the way - // that we cache entities: http://issues.umbraco.org/issue/U4-4259 - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); - throw; - } + var casted = (TEntity)entity; + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.CreateOrUpdate(casted, PersistUpdatedItem); + } } /// @@ -340,21 +260,19 @@ namespace Umbraco.Core.Persistence.Repositories /// public virtual void PersistDeletedItem(IEntity entity) { - PersistDeletedItem((TEntity)entity); - RuntimeCache.ClearCacheItem(GetCacheIdKey(entity.Id)); - //If there's a GetAll zero count cache, ensure it is cleared - RuntimeCache.ClearCacheItem(GetCacheTypeKey()); + var casted = (TEntity)entity; + + using (var p = CachePolicyFactory.CreatePolicy()) + { + p.Remove(casted, PersistDeletedItem); + } } - - #endregion - - #region Abstract IUnitOfWorkRepository Methods + protected abstract void PersistNewItem(TEntity item); protected abstract void PersistUpdatedItem(TEntity item); protected abstract void PersistDeletedItem(TEntity item); - #endregion /// /// Dispose disposable properties diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index 0a0dbe37dd..fa780e1bd0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; @@ -32,7 +33,6 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ITemplatesSection _templateConfig; private readonly ViewHelper _viewHelper; private readonly MasterPageHelper _masterPageHelper; - private readonly RepositoryCacheOptions _cacheOptions; internal TemplateRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IFileSystem masterpageFileSystem, IFileSystem viewFileSystem, ITemplatesSection templateConfig) : base(work, cache, logger, sqlSyntax) @@ -41,41 +41,27 @@ namespace Umbraco.Core.Persistence.Repositories _viewsFileSystem = viewFileSystem; _templateConfig = templateConfig; _viewHelper = new ViewHelper(_viewsFileSystem); - _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); - - _cacheOptions = new RepositoryCacheOptions - { - //Allow a zero count cache entry because GetAll() gets used quite a lot and we want to ensure - // if there are no templates, that it doesn't keep going to the db. - GetAllCacheAllowZeroCount = true, - //GetAll gets called a lot, we want to ensure that all templates are in the cache, default is 100 which - // would normally be fine but we'll increase it in case people have a ton of templates. - GetAllCacheThresholdLimit = 500 - }; + _masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); } - /// - /// Returns the repository cache options - /// - protected override RepositoryCacheOptions RepositoryCacheOptions + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { - get { return _cacheOptions; } + get + { + //Use a FullDataSet cache policy - this will cache the entire GetAll result in a single collection + return _cachePolicyFactory ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), false)); + } } #region Overrides of RepositoryBase protected override ITemplate PerformGet(int id) { - var sql = GetBaseQuery(false).Where(x => x.NodeId == id); - var result = Database.Fetch(sql).FirstOrDefault(); - if (result == null) return null; - - //look up the simple template definitions that have a master template assigned, this is used - // later to populate the template item's properties - var childIds = GetAxisDefinitions(result).ToArray(); - - return MapFromDto(result, childIds); + //use the underlying GetAll which will force cache all templates + return base.GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) @@ -479,126 +465,102 @@ namespace Umbraco.Core.Persistence.Repositories public ITemplate Get(string alias) { - var sql = GetBaseQuery(false).Where(x => x.Alias == alias); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - return MapFromDto(dto, GetAxisDefinitions(dto).ToArray()); + return GetAll(alias).FirstOrDefault(); } public IEnumerable GetAll(params string[] aliases) { - var sql = GetBaseQuery(false); + //We must call the base (normal) GetAll method + // which is cached. This is a specialized method and unfortunatley with the params[] it + // overlaps with the normal GetAll method. + if (aliases.Any() == false) return base.GetAll(); - if (aliases.Any()) - { - sql.Where("cmsTemplate.alias IN (@aliases)", new {aliases = aliases}); - } - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + //return from base.GetAll, this is all cached + return base.GetAll().Where(x => aliases.InvariantContains(x.Alias)); } public IEnumerable GetChildren(int masterTemplateId) { - var sql = GetBaseQuery(false); - if (masterTemplateId <= 0) - { - sql.Where(x => x.ParentId <= 0); - } - else - { - sql.Where(x => x.ParentId == masterTemplateId); - } + //return from base.GetAll, this is all cached + var all = base.GetAll().ToArray(); - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); + if (masterTemplateId <= 0) return all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace()); - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + var parent = all.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) return Enumerable.Empty(); + + var children = all.Where(x => x.MasterTemplateAlias.InvariantEquals(parent.Alias)); + return children; } public IEnumerable GetChildren(string alias) { - var sql = GetBaseQuery(false); - if (alias.IsNullOrWhiteSpace()) - { - sql.Where(x => x.ParentId <= 0); - } - else - { - //unfortunately SQLCE doesn't support scalar subqueries in the where clause, otherwise we could have done this - // in a single query, now we have to lookup the path to acheive the same thing - var parent = Database.ExecuteScalar(new Sql().Select("nodeId").From(SqlSyntax).Where(dto => dto.Alias == alias)); - if (parent.HasValue == false) return Enumerable.Empty(); - - sql.Where(x => x.ParentId == parent.Value); - } - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + //return from base.GetAll, this is all cached + return base.GetAll().Where(x => alias.IsNullOrWhiteSpace() + ? x.MasterTemplateAlias.IsNullOrWhiteSpace() + : x.MasterTemplateAlias.InvariantEquals(alias)); } public IEnumerable GetDescendants(int masterTemplateId) { - var sql = GetBaseQuery(false); + //return from base.GetAll, this is all cached + var all = base.GetAll().ToArray(); + var descendants = new List(); if (masterTemplateId > 0) { - //unfortunately SQLCE doesn't support scalar subqueries in the where clause, otherwise we could have done this - // in a single query, now we have to lookup the path to acheive the same thing - var path = Database.ExecuteScalar( - new Sql().Select(SqlSyntax.GetQuotedColumnName("path")) - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, dto => dto.NodeId, dto => dto.NodeId) - .Where(dto => dto.NodeId == masterTemplateId)); - - if (path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - sql.Where(@"(umbracoNode." + SqlSyntax.GetQuotedColumnName("path") + @" LIKE @query)", new { query = path + ",%" }); + var parent = all.FirstOrDefault(x => x.Id == masterTemplateId); + if (parent == null) return Enumerable.Empty(); + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); + } + else + { + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (var parent in descendants) + { + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); + } } - sql.OrderBy("umbracoNode." + SqlSyntax.GetQuotedColumnName("level")); - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); - + //return the list - it will be naturally ordered by level + return descendants; } - + public IEnumerable GetDescendants(string alias) { - var sql = GetBaseQuery(false); + var all = base.GetAll().ToArray(); + var descendants = new List(); if (alias.IsNullOrWhiteSpace() == false) { - //unfortunately SQLCE doesn't support scalar subqueries in the where clause, otherwise we could have done this - // in a single query, now we have to lookup the path to acheive the same thing - var path = Database.ExecuteScalar( - "SELECT umbracoNode.path FROM cmsTemplate INNER JOIN umbracoNode ON cmsTemplate.nodeId = umbracoNode.id WHERE cmsTemplate.alias = @alias", new { alias = alias }); - - if (path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - sql.Where(@"(umbracoNode." + SqlSyntax.GetQuotedColumnName("path") + @" LIKE @query)", new {query = path + ",%" }); + var parent = all.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + if (parent == null) return Enumerable.Empty(); + //recursively add all children + AddChildren(all, descendants, parent.Alias); } + else + { + descendants.AddRange(all.Where(x => x.MasterTemplateAlias.IsNullOrWhiteSpace())); + foreach (var parent in descendants) + { + //recursively add all children with a level + AddChildren(all, descendants, parent.Alias); + } + } + //return the list - it will be naturally ordered by level + return descendants; + } - sql.OrderBy("umbracoNode." + SqlSyntax.GetQuotedColumnName("level")); - - var dtos = Database.Fetch(sql).ToArray(); - if (dtos.Length == 0) return Enumerable.Empty(); - - var axisDefos = GetAxisDefinitions(dtos).ToArray(); - return dtos.Select(x => MapFromDto(x, axisDefos)); + private void AddChildren(ITemplate[] all, List descendants, string masterAlias) + { + var c = all.Where(x => x.MasterTemplateAlias.InvariantEquals(masterAlias)).ToArray(); + descendants.AddRange(c); + if (c.Any() == false) return; + //recurse through all children + foreach (var child in c) + { + AddChildren(all, descendants, child.Alias); + } } /// @@ -610,9 +572,9 @@ namespace Umbraco.Core.Persistence.Repositories public TemplateNode GetTemplateNode(string alias) { //first get all template objects - var allTemplates = GetAll().ToArray(); + var allTemplates = base.GetAll().ToArray(); - var selfTemplate = allTemplates.SingleOrDefault(x => x.Alias == alias); + var selfTemplate = allTemplates.SingleOrDefault(x => x.Alias.InvariantEquals(alias)); if (selfTemplate == null) { return null; @@ -621,11 +583,11 @@ namespace Umbraco.Core.Persistence.Repositories var top = selfTemplate; while (top.MasterTemplateAlias.IsNullOrWhiteSpace() == false) { - top = allTemplates.Single(x => x.Alias == top.MasterTemplateAlias); + top = allTemplates.Single(x => x.Alias.InvariantEquals(top.MasterTemplateAlias)); } var topNode = new TemplateNode(allTemplates.Single(x => x.Id == top.Id)); - var childTemplates = allTemplates.Where(x => x.MasterTemplateAlias == top.Alias); + var childTemplates = allTemplates.Where(x => x.MasterTemplateAlias.InvariantEquals(top.Alias)); //This now creates the hierarchy recursively topNode.Children = CreateChildren(topNode, childTemplates, allTemplates); @@ -633,10 +595,11 @@ namespace Umbraco.Core.Persistence.Repositories return FindTemplateInTree(topNode, alias); } + [Obsolete("Only used by obsolete code")] private static TemplateNode WalkTree(TemplateNode current, string alias) { //now walk the tree to find the node - if (current.Template.Alias == alias) + if (current.Template.Alias.InvariantEquals(alias)) { return current; } @@ -682,14 +645,16 @@ namespace Umbraco.Core.Persistence.Repositories public RenderingEngine DetermineTemplateRenderingEngine(ITemplate template) { var engine = _templateConfig.DefaultRenderingEngine; - - if (template.Content.IsNullOrWhiteSpace() == false && MasterPageHelper.IsMasterPageSyntax(template.Content)) + var viewHelper = new ViewHelper(_viewsFileSystem); + if (viewHelper.ViewExists(template) == false) { - //there is a design but its definitely a webforms design - return RenderingEngine.WebForms; + if (template.Content.IsNullOrWhiteSpace() == false && MasterPageHelper.IsMasterPageSyntax(template.Content)) + { + //there is a design but its definitely a webforms design and we haven't got a MVC view already for it + return RenderingEngine.WebForms; + } } - var viewHelper = new ViewHelper(_viewsFileSystem); var masterPageHelper = new MasterPageHelper(_masterpagesFileSystem); switch (engine) @@ -766,7 +731,7 @@ namespace Umbraco.Core.Persistence.Repositories //get this node's children var local = childTemplate; - var kids = allTemplates.Where(x => x.MasterTemplateAlias == local.Alias); + var kids = allTemplates.Where(x => x.MasterTemplateAlias.InvariantEquals(local.Alias)); //recurse child.Children = CreateChildren(child, kids, allTemplates); @@ -796,7 +761,7 @@ namespace Umbraco.Core.Persistence.Repositories private bool AliasAlreadExists(ITemplate template) { - var sql = GetBaseQuery(true).Where(x => x.Alias == template.Alias && x.NodeId != template.Id); + var sql = GetBaseQuery(true).Where(x => x.Alias.InvariantEquals(template.Alias) && x.NodeId != template.Id); var count = Database.ExecuteScalar(sql); return count > 0; } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index a7de116470..4b2befb3e3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -24,6 +25,8 @@ using Umbraco.Core.IO; namespace Umbraco.Core.Persistence.Repositories { + using SqlSyntax; + internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { @@ -60,11 +63,11 @@ namespace Umbraco.Core.Persistence.Repositories public virtual void DeleteVersion(Guid versionId) { var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); - if(dto == null) return; + if (dto == null) return; //Ensure that the lastest version is not deleted var latestVersionDto = Database.FirstOrDefault("WHERE ContentId = @Id ORDER BY VersionDate DESC", new { Id = dto.NodeId }); - if(latestVersionDto.VersionId == dto.VersionId) + if (latestVersionDto.VersionId == dto.VersionId) return; using (var transaction = Database.GetTransaction()) @@ -82,7 +85,7 @@ namespace Umbraco.Core.Persistence.Repositories var list = Database.Fetch( "WHERE versionId <> @VersionId AND (ContentId = @Id AND VersionDate < @VersionDate)", - new {VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate}); + new { VersionId = latestVersionDto.VersionId, Id = id, VersionDate = versionDate }); if (list.Any() == false) return; using (var transaction = Database.GetTransaction()) @@ -243,25 +246,68 @@ namespace Umbraco.Core.Persistence.Repositories return filteredSql; } - private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy) + private Sql GetSortedSqlForPagedResults(Sql sql, Direction orderDirection, string orderBy, bool orderBySystemField) { + //copy to var so that the original isn't changed var sortedSql = new Sql(sql.SQL, sql.Arguments); - // Apply order according to parameters - if (string.IsNullOrEmpty(orderBy) == false) + + if (orderBySystemField) { - var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; - if (orderDirection == Direction.Ascending) + // Apply order according to parameters + if (string.IsNullOrEmpty(orderBy) == false) { - sortedSql.OrderBy(orderByParams); + var orderByParams = new[] { GetDatabaseFieldNameForOrderBy(orderBy) }; + if (orderDirection == Direction.Ascending) + { + sortedSql.OrderBy(orderByParams); + } + else + { + sortedSql.OrderByDescending(orderByParams); + } } - else - { - sortedSql.OrderByDescending(orderByParams); - } - return sortedSql; } + else + { + // Sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through valie + // from most recent content version for the given order by field + var sortedInt = string.Format(SqlSyntax.ConvertIntegerToOrderableString, "dataInt"); + var sortedDate = string.Format(SqlSyntax.ConvertDateToOrderableString, "dataDate"); + var sortedString = string.Format("COALESCE({0},'')", "dataNvarchar"); + var sortedDecimal = string.Format(SqlSyntax.ConvertDecimalToOrderableString, "dataDecimal"); + + var innerJoinTempTable = string.Format(@"INNER JOIN ( + SELECT CASE + WHEN dataInt Is Not Null THEN {0} + WHEN dataDecimal Is Not Null THEN {1} + WHEN dataDate Is Not Null THEN {2} + ELSE {3} + END AS CustomPropVal, + cd.nodeId AS CustomPropValContentId + FROM cmsDocument cd + INNER JOIN cmsPropertyData cpd ON cpd.contentNodeId = cd.nodeId AND cpd.versionId = cd.versionId + INNER JOIN cmsPropertyType cpt ON cpt.Id = cpd.propertytypeId + WHERE cpt.Alias = @{4} AND cd.newest = 1) AS CustomPropData + ON CustomPropData.CustomPropValContentId = umbracoNode.id + ", sortedInt, sortedDecimal, sortedDate, sortedString, sortedSql.Arguments.Length); + + //insert this just above the LEFT OUTER JOIN + var newSql = sortedSql.SQL.Insert(sortedSql.SQL.IndexOf("LEFT OUTER JOIN"), innerJoinTempTable); + var newArgs = sortedSql.Arguments.ToList(); + newArgs.Add(orderBy); + + sortedSql = new Sql(newSql, newArgs.ToArray()); + + sortedSql.OrderBy("CustomPropData.CustomPropVal"); + if (orderDirection == Direction.Descending) + { + sortedSql.Append(" DESC"); + } + } + return sortedSql; + } /// @@ -278,13 +324,15 @@ namespace Umbraco.Core.Persistence.Repositories /// A callback to process the query result /// The order by column /// The order direction. + /// Flag to indicate when ordering by system field /// /// orderBy protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Tuple nodeIdSelect, Func> processQuery, - string orderBy, + string orderBy, Direction orderDirection, + bool orderBySystemField, Func> defaultFilter = null) where TContentBase : class, IAggregateRoot, TEntity { @@ -296,18 +344,18 @@ namespace Umbraco.Core.Persistence.Repositories if (query == null) query = new Query(); var translator = new SqlTranslator(sqlBase, query); var sqlQuery = translator.Translate(); - + // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. // So we'll modify the SQL. var sqlNodeIds = new Sql( - sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}",nodeIdSelect.Item1, nodeIdSelect.Item2)), + sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), sqlQuery.Arguments); - + //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), - orderDirection, orderBy); + orderDirection, orderBy, orderBySystemField); // Get page of results and total count IEnumerable result; @@ -323,7 +371,7 @@ namespace Umbraco.Core.Persistence.Repositories var args = sqlNodeIdsWithSort.Arguments; string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - + //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId sqlStringPage = sqlStringPage .Replace("SELECT *", @@ -332,7 +380,7 @@ namespace Umbraco.Core.Persistence.Repositories "SELECT " + nodeIdSelect.Item2); //We need to make this an inner join on the paged query - var splitQuery = sqlQuery.SQL.Split(new[] {"WHERE "}, StringSplitOptions.None); + var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); var withInnerJoinSql = new Sql(splitQuery[0]) .Append("INNER JOIN (") //join the paged query with the paged query arguments @@ -344,23 +392,9 @@ namespace Umbraco.Core.Persistence.Repositories //get sorted and filtered sql var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), - orderDirection, orderBy); - - var content = processQuery(fullQuery) - .Cast() - .AsQueryable(); - - // Now we need to ensure this result is also ordered by the same order by clause - var orderByProperty = GetEntityPropertyNameForOrderBy(orderBy); - if (orderDirection == Direction.Ascending) - { - result = content.OrderBy(orderByProperty); - } - else - { - result = content.OrderByDescending(orderByProperty); - } + GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), + orderDirection, orderBy, orderBySystemField); + return processQuery(fullQuery); } else { @@ -393,9 +427,9 @@ INNER JOIN (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId LEFT OUTER JOIN cmsDataTypePreValues -ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); +ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); - var allPropertyData = Database.Fetch(propSql); + var allPropertyData = Database.Fetch(propSql); //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use // below if any property requires tag support @@ -413,7 +447,7 @@ WHERE EXISTS( ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); - return Database.Fetch(preValsSql); + return Database.Fetch(preValsSql); }); var result = new Dictionary(); @@ -431,8 +465,8 @@ WHERE EXISTS( var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); - + var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); + var newProperties = properties.Where(x => x.HasIdentity == false && x.PropertyType.HasIdentity); foreach (var property in newProperties) @@ -480,7 +514,7 @@ WHERE EXISTS( Logger.Warn>("The query returned multiple property sets for document definition " + def.Id + ", " + def.Composition.Name); } result[def.Id] = new PropertyCollection(properties); - } + } } return result; @@ -520,8 +554,12 @@ WHERE EXISTS( case "OWNER": //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return "umbracoNode.nodeUser"; + // Members only + case "USERNAME": + return "cmsMember.LoginName"; default: - return orderBy; + //ensure invalid SQL cannot be submitted + return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); } } @@ -540,7 +578,8 @@ WHERE EXISTS( case "VERSIONDATE": return "UpdateDate"; default: - return orderBy; + //ensure invalid SQL cannot be submitted + return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", ""); } } diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index f9367d8433..e2b2c2cc22 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -1,5 +1,6 @@ using Umbraco.Core.Configuration; using System; +using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -18,6 +19,7 @@ namespace Umbraco.Core.Persistence private readonly ILogger _logger; private readonly ISqlSyntaxProvider _sqlSyntax; private readonly CacheHelper _cacheHelper; + private readonly CacheHelper _noCache; private readonly IUmbracoSettingsSection _settings; #region Ctors @@ -29,7 +31,28 @@ namespace Umbraco.Core.Persistence //if (sqlSyntax == null) throw new ArgumentNullException("sqlSyntax"); if (settings == null) throw new ArgumentNullException("settings"); - _cacheHelper = cacheHelper; + _cacheHelper = cacheHelper; + + //IMPORTANT: We will force the DeepCloneRuntimeCacheProvider to be used here which is a wrapper for the underlying + // runtime cache to ensure that anything that can be deep cloned in/out is done so, this also ensures that our tracks + // changes entities are reset. + if ((_cacheHelper.RuntimeCache is DeepCloneRuntimeCacheProvider) == false) + { + var origRuntimeCache = cacheHelper.RuntimeCache; + _cacheHelper.RuntimeCache = new DeepCloneRuntimeCacheProvider(origRuntimeCache); + } + //If the factory for isolated cache doesn't return DeepCloneRuntimeCacheProvider, then ensure it does + if (_cacheHelper.IsolatedRuntimeCache.CacheFactory.Method.ReturnType != typeof (DeepCloneRuntimeCacheProvider)) + { + var origFactory = cacheHelper.IsolatedRuntimeCache.CacheFactory; + _cacheHelper.IsolatedRuntimeCache.CacheFactory = type => + { + var cache = origFactory(type); + return new DeepCloneRuntimeCacheProvider(cache); + }; + } + + _noCache = CacheHelper.CreateDisabledCacheHelper(); _logger = logger; _sqlSyntax = sqlSyntax; _settings = settings; @@ -53,6 +76,7 @@ namespace Umbraco.Core.Persistence { if (cacheHelper == null) throw new ArgumentNullException("cacheHelper"); _cacheHelper = cacheHelper; + _noCache = CacheHelper.CreateDisabledCacheHelper(); } [Obsolete("Use the ctor specifying all dependencies instead")] @@ -80,15 +104,15 @@ namespace Umbraco.Core.Persistence public virtual ITaskRepository CreateTaskRepository(IDatabaseUnitOfWork uow) { - return new TaskRepository(uow, - CacheHelper.CreateDisabledCacheHelper(), //never cache + return new TaskRepository(uow, + _noCache, //never cache _logger, _sqlSyntax); } public virtual IAuditRepository CreateAuditRepository(IDatabaseUnitOfWork uow) { return new AuditRepository(uow, - CacheHelper.CreateDisabledCacheHelper(), //never cache + _noCache, //never cache _logger, _sqlSyntax); } @@ -128,7 +152,6 @@ namespace Umbraco.Core.Persistence { return new DataTypeDefinitionRepository( uow, - _cacheHelper, _cacheHelper, _logger, _sqlSyntax, CreateContentTypeRepository(uow)); @@ -140,8 +163,7 @@ namespace Umbraco.Core.Persistence uow, _cacheHelper, _logger, - _sqlSyntax, - CreateLanguageRepository(uow)); + _sqlSyntax); } public virtual ILanguageRepository CreateLanguageRepository(IDatabaseUnitOfWork uow) @@ -175,7 +197,7 @@ namespace Umbraco.Core.Persistence { return new RelationRepository( uow, - CacheHelper.CreateDisabledCacheHelper(), //never cache + _noCache, //never cache _logger, _sqlSyntax, CreateRelationTypeRepository(uow)); } @@ -184,7 +206,7 @@ namespace Umbraco.Core.Persistence { return new RelationTypeRepository( uow, - CacheHelper.CreateDisabledCacheHelper(), //never cache + _noCache, //never cache _logger, _sqlSyntax); } @@ -222,7 +244,7 @@ namespace Umbraco.Core.Persistence { return new MigrationEntryRepository( uow, - CacheHelper.CreateDisabledCacheHelper(), //never cache + _noCache, //never cache _logger, _sqlSyntax); } @@ -283,8 +305,7 @@ namespace Umbraco.Core.Persistence { return new MemberGroupRepository(uow, _cacheHelper, - _logger, _sqlSyntax, - _cacheHelper); + _logger, _sqlSyntax); } public virtual IEntityRepository CreateEntityRepository(IDatabaseUnitOfWork uow) @@ -292,16 +313,25 @@ namespace Umbraco.Core.Persistence return new EntityRepository(uow); } - public IDomainRepository CreateDomainRepository(IDatabaseUnitOfWork uow) + public virtual IDomainRepository CreateDomainRepository(IDatabaseUnitOfWork uow) { return new DomainRepository(uow, _cacheHelper, _logger, _sqlSyntax); } public ITaskTypeRepository CreateTaskTypeRepository(IDatabaseUnitOfWork uow) { - return new TaskTypeRepository(uow, - CacheHelper.CreateDisabledCacheHelper(), //never cache + return new TaskTypeRepository(uow, + _noCache, //never cache _logger, _sqlSyntax); } + + internal virtual EntityContainerRepository CreateEntityContainerRepository(IDatabaseUnitOfWork uow, Guid containerObjectType) + { + return new EntityContainerRepository( + uow, + _cacheHelper, + _logger, _sqlSyntax, + containerObjectType); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 056d9c1d9a..e61b23c8ec 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -69,6 +69,10 @@ namespace Umbraco.Core.Persistence.SqlSyntax bool SupportsClustered(); bool SupportsIdentityInsert(); bool? SupportsCaseInsensitiveQueries(Database db); + + string ConvertIntegerToOrderableString { get; } + string ConvertDateToOrderableString { get; } + string ConvertDecimalToOrderableString { get; } IEnumerable GetTablesInSchema(Database db); IEnumerable GetColumnsInSchema(Database db); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 84c6e6e824..a6d1d690ba 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -10,6 +10,19 @@ namespace Umbraco.Core.Persistence.SqlSyntax public abstract class MicrosoftSqlSyntaxProviderBase : SqlSyntaxProviderBase where TSyntax : ISqlSyntaxProvider { + protected MicrosoftSqlSyntaxProviderBase() + { + AutoIncrementDefinition = "IDENTITY(1,1)"; + GuidColumnDefinition = "UniqueIdentifier"; + RealColumnDefinition = "FLOAT"; + BoolColumnDefinition = "BIT"; + DecimalColumnDefinition = "DECIMAL(38,6)"; + TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ + BlobColumnDefinition = "VARBINARY(MAX)"; + + InitColumnTypeMap(); + } + public override string RenameTable { get { return "sp_rename '{0}', '{1}'"; } } public override string AddColumn { get { return "ALTER TABLE {0} ADD {1}"; } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs index 1b96855a3d..7167a5af95 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MySqlSyntaxProvider.cs @@ -18,9 +18,6 @@ namespace Umbraco.Core.Persistence.SqlSyntax public MySqlSyntaxProvider(ILogger logger) { _logger = logger; - DefaultStringLength = 255; - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); AutoIncrementDefinition = "AUTO_INCREMENT"; IntColumnDefinition = "int(11)"; @@ -30,9 +27,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax DecimalColumnDefinition = "decimal(38,6)"; GuidColumnDefinition = "char(36)"; - InitColumnTypeMap(); - DefaultValueFormat = "DEFAULT {0}"; + + InitColumnTypeMap(); } public override IEnumerable GetTablesInSchema(Database db) @@ -329,13 +326,13 @@ ORDER BY TABLE_NAME, INDEX_NAME", { case SystemMethods.NewGuid: return null; // NOT SUPPORTED! - //return "NEWID()"; + //return "NEWID()"; case SystemMethods.CurrentDateTime: return "CURRENT_TIMESTAMP"; - //case SystemMethods.NewSequentialId: - // return "NEWSEQUENTIALID()"; - //case SystemMethods.CurrentUTCDateTime: - // return "GETUTCDATE()"; + //case SystemMethods.NewSequentialId: + // return "NEWSEQUENTIALID()"; + //case SystemMethods.CurrentUTCDateTime: + // return "GETUTCDATE()"; } return null; @@ -363,6 +360,9 @@ ORDER BY TABLE_NAME, INDEX_NAME", public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } public override string RenameColumn { get { return "ALTER TABLE {0} CHANGE {1} {2}"; } } + public override string ConvertIntegerToOrderableString { get { return "LPAD(FORMAT({0}, 0), 8, '0')"; } } + public override string ConvertDateToOrderableString { get { return "DATE_FORMAT({0}, '%Y%m%d')"; } } + public override string ConvertDecimalToOrderableString { get { return "LPAD(FORMAT({0}, 9), 20, '0')"; } } public override bool? SupportsCaseInsensitiveQueries(Database db) { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index 4ac513a110..23e0a1bb87 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -15,19 +15,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax { public SqlCeSyntaxProvider() { - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - - AutoIncrementDefinition = "IDENTITY(1,1)"; - StringColumnDefinition = "NVARCHAR(255)"; - GuidColumnDefinition = "UniqueIdentifier"; - RealColumnDefinition = "FLOAT"; - BoolColumnDefinition = "BIT"; - DecimalColumnDefinition = "DECIMAL(38,6)"; - TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ - BlobColumnDefinition = "VARBINARY(MAX)"; - - InitColumnTypeMap(); + } public override bool SupportsClustered() @@ -220,7 +208,6 @@ ORDER BY TABLE_NAME, INDEX_NAME"); public override string DropIndex { get { return "DROP INDEX {1}.{0}"; } } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 53198cdf5e..9cb0ce327d 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -13,19 +13,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax { public SqlServerSyntaxProvider() { - StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; - StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); - - AutoIncrementDefinition = "IDENTITY(1,1)"; - StringColumnDefinition = "VARCHAR(8000)"; - GuidColumnDefinition = "UniqueIdentifier"; - RealColumnDefinition = "FLOAT"; - BoolColumnDefinition = "BIT"; - DecimalColumnDefinition = "DECIMAL(38,6)"; - TimeColumnDefinition = "TIME"; //SQLSERVER 2008+ - BlobColumnDefinition = "VARBINARY(MAX)"; - - InitColumnTypeMap(); + } /// diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 7c08e34a98..50417761aa 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -32,6 +32,13 @@ namespace Umbraco.Core.Persistence.SqlSyntax FormatPrimaryKey, FormatIdentity }; + + //defaults for all providers + StringLengthColumnDefinitionFormat = StringLengthUnicodeColumnDefinitionFormat; + StringColumnDefinition = string.Format(StringLengthColumnDefinitionFormat, DefaultStringLength); + DecimalColumnDefinition = string.Format(DecimalColumnDefinitionFormat, DefaultDecimalPrecision, DefaultDecimalScale); + + InitColumnTypeMap(); } public string GetWildcardPlaceholder() @@ -41,9 +48,12 @@ namespace Umbraco.Core.Persistence.SqlSyntax public string StringLengthNonUnicodeColumnDefinitionFormat = "VARCHAR({0})"; public string StringLengthUnicodeColumnDefinitionFormat = "NVARCHAR({0})"; + public string DecimalColumnDefinitionFormat = "DECIMAL({0},{1})"; public string DefaultValueFormat = "DEFAULT ({0})"; public int DefaultStringLength = 255; + public int DefaultDecimalPrecision = 20; + public int DefaultDecimalScale = 9; //Set by Constructor public string StringColumnDefinition; @@ -55,7 +65,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax public string GuidColumnDefinition = "GUID"; public string BoolColumnDefinition = "BOOL"; public string RealColumnDefinition = "DOUBLE"; - public string DecimalColumnDefinition = "DECIMAL"; + public string DecimalColumnDefinition; public string BlobColumnDefinition = "BLOB"; public string DateTimeColumnDefinition = "DATETIME"; public string TimeColumnDefinition = "DATETIME"; @@ -312,7 +322,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax GetQuotedColumnName(foreignKey.ForeignColumns.First()), GetQuotedTableName(foreignKey.PrimaryTable), GetQuotedColumnName(foreignKey.PrimaryColumns.First()), - FormatCascade("DELETE", foreignKey.OnDelete), + FormatCascade("DELETE", foreignKey.OnDelete), FormatCascade("UPDATE", foreignKey.OnUpdate)); } @@ -321,7 +331,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax var sb = new StringBuilder(); foreach (var column in columns) { - sb.Append(Format(column) +",\n"); + sb.Append(Format(column) + ",\n"); } return sb.ToString().TrimEnd(",\n"); } @@ -421,16 +431,23 @@ namespace Umbraco.Core.Persistence.SqlSyntax return GetSpecialDbType(column.DbType); } - Type type = column.Type.HasValue + Type type = column.Type.HasValue ? DbTypeMap.ColumnDbTypeMap.First(x => x.Value == column.Type.Value).Key : column.PropertyType; - if (type == typeof (string)) + if (type == typeof(string)) { var valueOrDefault = column.Size != default(int) ? column.Size : DefaultStringLength; return string.Format(StringLengthColumnDefinitionFormat, valueOrDefault); } + if (type == typeof(decimal)) + { + var precision = column.Size != default(int) ? column.Size : DefaultDecimalPrecision; + var scale = column.Precision != default(int) ? column.Precision : DefaultDecimalScale; + return string.Format(DecimalColumnDefinitionFormat, precision, scale); + } + string definition = DbTypeMap.ColumnTypeMap.First(x => x.Key == type).Value; string dbTypeDefinition = column.Size != default(int) ? string.Format("{0}({1})", definition, column.Size) @@ -519,5 +536,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public virtual string CreateConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} {2} ({3})"; } } public virtual string DeleteConstraint { get { return "ALTER TABLE {0} DROP CONSTRAINT {1}"; } } public virtual string CreateForeignKeyConstraint { get { return "ALTER TABLE {0} ADD CONSTRAINT {1} FOREIGN KEY ({2}) REFERENCES {3} ({4}){5}{6}"; } } + + public virtual string ConvertIntegerToOrderableString { get { return "REPLACE(STR({0}, 8), SPACE(1), '0')"; } } + public virtual string ConvertDateToOrderableString { get { return "CONVERT(nvarchar, {0}, 102)"; } } + public virtual string ConvertDecimalToOrderableString { get { return "REPLACE(STR({0}, 20, 9), SPACE(1), '0')"; } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 1d60879f97..a0490348f3 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; +using System.Text; using StackExchange.Profiling; using Umbraco.Core.Logging; @@ -12,9 +13,9 @@ namespace Umbraco.Core.Persistence /// Represents the Umbraco implementation of the PetaPoco Database object /// /// - /// Currently this object exists for 'future proofing' our implementation. By having our own inheritied implementation we + /// Currently this object exists for 'future proofing' our implementation. By having our own inheritied implementation we /// can then override any additional execution (such as additional loggging, functionality, etc...) that we need to without breaking compatibility since we'll always be exposing - /// this object instead of the base PetaPoco database object. + /// this object instead of the base PetaPoco database object. /// public class UmbracoDatabase : Database, IDisposeOnRequestEnd { @@ -111,22 +112,42 @@ namespace Umbraco.Core.Persistence public override IDbConnection OnConnectionOpened(IDbConnection connection) { - // wrap the connection with a profiling connection that tracks timings + // propagate timeout if none yet + + // wrap the connection with a profiling connection that tracks timings return new StackExchange.Profiling.Data.ProfiledDbConnection(connection as DbConnection, MiniProfiler.Current); } public override void OnException(Exception x) { - _logger.Info(x.StackTrace); + _logger.Error("Database exception occurred", x); base.OnException(x); } + public override void OnExecutingCommand(IDbCommand cmd) + { + // if no timeout is specified, and the connection has a longer timeout, use it + if (OneTimeCommandTimeout == 0 && CommandTimeout == 0 && cmd.Connection.ConnectionTimeout > 30) + cmd.CommandTimeout = cmd.Connection.ConnectionTimeout; + + if (EnableSqlTrace) + { + var sb = new StringBuilder(); + sb.Append(cmd.CommandText); + foreach (DbParameter p in cmd.Parameters) + { + sb.Append(" - "); + sb.Append(p.Value); + } + + _logger.Debug(sb.ToString()); + } + + base.OnExecutingCommand(cmd); + } + public override void OnExecutedCommand(IDbCommand cmd) { - if (EnableSqlTrace) - { - _logger.Debug(cmd.CommandText); - } if (_enableCount) { SqlCount++; diff --git a/src/Umbraco.Core/PropertyEditors/DecimalValidator.cs b/src/Umbraco.Core/PropertyEditors/DecimalValidator.cs new file mode 100644 index 0000000000..632821bc24 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/DecimalValidator.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// A validator that validates that the value is a valid decimal + /// + [ValueValidator("Decimal")] + internal sealed class DecimalValidator : ManifestValueValidator, IPropertyValidator + { + public override IEnumerable Validate(object value, string config, PreValueCollection preValues, PropertyEditor editor) + { + if (value != null && value.ToString() != string.Empty) + { + var result = value.TryConvertTo(); + if (result.Success == false) + { + yield return new ValidationResult("The value " + value + " is not a valid decimal", new[] { "value" }); + } + } + } + + public IEnumerable Validate(object value, PreValueCollection preValues, PropertyEditor editor) + { + return Validate(value, "", preValues, editor); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs index 770ee65aed..c7cbc0f473 100644 --- a/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/IPropertyEditorValueConverter.cs @@ -2,6 +2,7 @@ using System; namespace Umbraco.Core.PropertyEditors { + /// /// Maps a property source value to a data object. /// // todo: drop IPropertyEditorValueConverter support (when?). diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs index 9b39f2be0b..6cecc9dff5 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs @@ -24,6 +24,10 @@ namespace Umbraco.Core.PropertyEditors /// public PropertyEditor() { + //defaults + Icon = Constants.Icons.PropertyEditor; + Group = "common"; + //assign properties based on the attribute if it is found _attribute = GetType().GetCustomAttribute(false); if (_attribute != null) @@ -32,6 +36,8 @@ namespace Umbraco.Core.PropertyEditors Alias = _attribute.Alias; Name = _attribute.Name; IsParameterEditor = _attribute.IsParameterEditor; + Icon = _attribute.Icon; + Group = _attribute.Group; } } @@ -65,6 +71,19 @@ namespace Umbraco.Core.PropertyEditors [JsonProperty("name", Required = Required.Always)] public string Name { get; internal set; } + /// + /// The icon of the property editor - if not set it uses a default icon + /// + [JsonProperty("icon")] + public string Icon { get; internal set; } + + /// + /// The group of the property editor - if not set the editor will list as a generic editor + /// + [JsonProperty("group")] + public string Group { get; internal set; } + + [JsonProperty("editor", Required = Required.Always)] public PropertyValueEditor ValueEditor { diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs index 714ff31abd..d120753185 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs @@ -19,7 +19,9 @@ namespace Umbraco.Core.PropertyEditors EditorView = editorView; //defaults - ValueType = "string"; + ValueType = PropertyEditorValueTypes.String; + Icon = Constants.Icons.PropertyEditor; + Group = "common"; } public PropertyEditorAttribute(string alias, string name) @@ -31,7 +33,9 @@ namespace Umbraco.Core.PropertyEditors Name = name; //defaults - ValueType = "string"; + ValueType = PropertyEditorValueTypes.String; + Icon = Constants.Icons.PropertyEditor; + Group = "common"; } public PropertyEditorAttribute(string alias, string name, string valueType, string editorView) @@ -45,6 +49,9 @@ namespace Umbraco.Core.PropertyEditors Name = name; ValueType = valueType; EditorView = editorView; + + Icon = Constants.Icons.PropertyEditor; + Group = "common"; } public string Alias { get; private set; } @@ -57,5 +64,16 @@ namespace Umbraco.Core.PropertyEditors /// If this is is true than the editor will be displayed full width without a label /// public bool HideLabel { get; set; } + + /// + /// Optional, If this is set, datatypes using the editor will display this icon instead of the default system one. + /// + public string Icon { get; set; } + + /// + /// Optional - if this is set, the datatype ui will display the editor in this group instead of the default one, by default an editor does not have a group. + /// The group has no effect on how a property editor is stored or referenced. + /// + public string Group { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs index 02834660ee..5c0227247e 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.IO; using Umbraco.Core.Manifest; @@ -17,12 +18,12 @@ namespace Umbraco.Core.PropertyEditors /// public class PropertyEditorResolver : LazyManyObjectsResolverBase { - public PropertyEditorResolver(IServiceProvider serviceProvider, ILogger logger, Func> typeListProducerList) + public PropertyEditorResolver(IServiceProvider serviceProvider, ILogger logger, Func> typeListProducerList, IRuntimeCacheProvider runtimeCache) : base(serviceProvider, logger, typeListProducerList, ObjectLifetimeScope.Application) { var builder = new ManifestBuilder( - ApplicationContext.Current.ApplicationCache.RuntimeCache, - new ManifestParser(new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), ApplicationContext.Current.ApplicationCache.RuntimeCache)); + runtimeCache, + new ManifestParser(new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), runtimeCache)); _unioned = new Lazy>(() => Values.Union(builder.PropertyEditors).ToList()); } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs index c81f243a32..a962923a0c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueConvertersResolver.cs @@ -8,6 +8,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Manages the list of IPropertyEditorValueConverter's /// + [Obsolete("IPropertyEditorValueConverter is obsolete, but we need to support them until v8, to resolve the correct converter types use: PropertyValueConvertersResolver")] internal sealed class PropertyEditorValueConvertersResolver : ManyObjectsResolverBase { /// diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorValueTypes.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueTypes.cs new file mode 100644 index 0000000000..0ece42d82b --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorValueTypes.cs @@ -0,0 +1,34 @@ +using System; + +namespace Umbraco.Core.PropertyEditors +{ + public static class PropertyEditorValueTypes + { + // mapped to DataTypeDatabaseType in DataTypeService.OverrideDatabaseTypeIfProvidedInPreValues + // BUT what about those that are not mapped? + // + // also mapped to DataTypeDatabaseType in PropertyValueEditor + // and this time the "+" are mapped + + public const string Date = "DATE"; // +Date + + public const string DateTime = "DATETIME"; // Date + + public const string Decimal = "DECIMAL"; // Decimal + + public const string Integer = "INT"; // Integer + + [Obsolete("Use Integer.", false)] + public const string IntegerAlternative = "INTEGER"; // +Integer + + public const string Json = "JSON"; // +NText + + public const string Text = "TEXT"; // NText + + public const string Time = "TIME"; // +Date + + public const string String = "STRING"; // NVarchar + + public const string Xml = "XML"; // +NText + } +} diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs index 8b00164059..0b083025f7 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueEditor.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.PropertyEditors /// public PropertyValueEditor() { - ValueType = "string"; + ValueType = PropertyEditorValueTypes.String; //set a default for validators Validators = new List(); } @@ -123,28 +123,35 @@ namespace Umbraco.Core.PropertyEditors } /// - /// Returns the true DataTypeDatabaseType from the string representation ValueType + /// Returns the true DataTypeDatabaseType from the string representation ValueType. /// /// public DataTypeDatabaseType GetDatabaseType() { - switch (ValueType.ToUpper(CultureInfo.InvariantCulture)) + return GetDatabaseType(ValueType); + } + + public static DataTypeDatabaseType GetDatabaseType(string valueType) + { + switch (valueType.ToUpperInvariant()) { - case "INT": - case "INTEGER": + case PropertyEditorValueTypes.Integer: + case PropertyEditorValueTypes.IntegerAlternative: return DataTypeDatabaseType.Integer; - case "STRING": + case PropertyEditorValueTypes.Decimal: + return DataTypeDatabaseType.Decimal; + case PropertyEditorValueTypes.String: return DataTypeDatabaseType.Nvarchar; - case "TEXT": - case "JSON": - case "XML": + case PropertyEditorValueTypes.Text: + case PropertyEditorValueTypes.Json: + case PropertyEditorValueTypes.Xml: return DataTypeDatabaseType.Ntext; - case "DATETIME": - case "DATE": - case "TIME": + case PropertyEditorValueTypes.DateTime: + case PropertyEditorValueTypes.Date: + case PropertyEditorValueTypes.Time: return DataTypeDatabaseType.Date; default: - throw new FormatException("The ValueType does not match a known value type"); + throw new ArgumentException("Not a valid value type.", "valueType"); } } @@ -202,6 +209,11 @@ namespace Umbraco.Core.PropertyEditors ? Attempt.Succeed((int)(long)result.Result) : result; + case DataTypeDatabaseType.Decimal: + //ensure these are nullable so we can return a null if required + valueType = typeof(decimal?); + break; + case DataTypeDatabaseType.Date: //ensure these are nullable so we can return a null if required valueType = typeof(DateTime?); @@ -231,7 +243,7 @@ namespace Umbraco.Core.PropertyEditors public virtual object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { //if it's json but it's empty json, then return null - if (ValueType.InvariantEquals("JSON") && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson()) + if (ValueType.InvariantEquals(PropertyEditorValueTypes.Json) && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson()) { return null; } @@ -283,8 +295,13 @@ namespace Umbraco.Core.PropertyEditors } return property.Value.ToString(); case DataTypeDatabaseType.Integer: - //we can just ToString() any of these types - return property.Value.ToString(); + case DataTypeDatabaseType.Decimal: + //Decimals need to be formatted with invariant culture (dots, not commas) + //Anything else falls back to ToString() + var decim = property.Value.TryConvertTo(); + return decim.Success + ? decim.Result.ToString(NumberFormatInfo.InvariantInfo) + : property.Value.ToString(); case DataTypeDatabaseType.Date: var date = property.Value.TryConvertTo(); if (date.Success == false || date.Result == null) @@ -325,6 +342,7 @@ namespace Umbraco.Core.PropertyEditors { case DataTypeDatabaseType.Date: case DataTypeDatabaseType.Integer: + case DataTypeDatabaseType.Decimal: return new XText(ConvertDbToString(property, propertyType, dataTypeService)); case DataTypeDatabaseType.Nvarchar: case DataTypeDatabaseType.Ntext: @@ -354,6 +372,7 @@ namespace Umbraco.Core.PropertyEditors property.Value.ToXmlString(); return property.Value.ToXmlString(); case DataTypeDatabaseType.Integer: + case DataTypeDatabaseType.Decimal: return property.Value.ToXmlString(property.Value.GetType()); case DataTypeDatabaseType.Date: //treat dates differently, output the format as xml format diff --git a/src/Umbraco.Core/PropertyEditors/RequiredManifestValueValidator.cs b/src/Umbraco.Core/PropertyEditors/RequiredManifestValueValidator.cs index 33c2c02cb6..b3e8ff1f6f 100644 --- a/src/Umbraco.Core/PropertyEditors/RequiredManifestValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/RequiredManifestValueValidator.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.PropertyEditors { var asString = value.ToString(); - if (editor.ValueEditor.ValueType.InvariantEquals("JSON")) + if (editor.ValueEditor.ValueType.InvariantEquals(PropertyEditorValueTypes.Json)) { if (asString.DetectIsEmptyJson()) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs new file mode 100644 index 0000000000..e32ff7af02 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DecimalValueConverter.cs @@ -0,0 +1,32 @@ +using System.Globalization; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + [PropertyValueType(typeof(decimal))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class DecimalValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return Constants.PropertyEditors.DecimalAlias.Equals(propertyType.PropertyEditorAlias); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return 0M; + + // in XML a decimal is a string + var sourceString = source as string; + if (sourceString != null) + { + decimal d; + return (decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out d)) ? d : 0M; + } + + // in the database an a decimal is an a decimal + // default value is zero + return (source is decimal) ? source : 0M; + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 11f915fabd..b3db026c89 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -12,6 +12,9 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.PropertyEditors.ValueConverters { + /// + /// This ensures that the grid config is merged in with the front-end value + /// [DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter [PropertyValueType(typeof(JToken))] [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] @@ -90,7 +93,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters } catch (Exception ex) { - LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs new file mode 100644 index 0000000000..3b018d8c10 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -0,0 +1,125 @@ +using System; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// This ensures that the cropper config (pre-values/crops) are merged in with the front-end value. + /// + [DefaultPropertyValueConverter(typeof (JsonValueConverter))] //this shadows the JsonValueConverter + [PropertyValueType(typeof (JToken))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class ImageCropperValueConverter : JsonValueConverter + { + private readonly IDataTypeService _dataTypeService; + + public ImageCropperValueConverter() + { + _dataTypeService = ApplicationContext.Current.Services.DataTypeService; + } + + public ImageCropperValueConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.ImageCropperAlias); + } + + internal static void MergePreValues(JObject currentValue, IDataTypeService dataTypeService, int dataTypeId) + { + //need to lookup the pre-values for this data type + //TODO: Change all singleton access to use ctor injection in v8!!! + var dt = dataTypeService.GetPreValuesCollectionByDataTypeId(dataTypeId); + + if (dt != null && dt.IsDictionaryBased && dt.PreValuesAsDictionary.ContainsKey("crops")) + { + var cropsString = dt.PreValuesAsDictionary["crops"].Value; + JArray preValueCrops; + try + { + preValueCrops = JsonConvert.DeserializeObject(cropsString); + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the string " + cropsString + " to a json object", ex); + return; + } + + //now we need to merge the crop values - the alias + width + height comes from pre-configured pre-values, + // however, each crop can store it's own coordinates + + JArray existingCropsArray; + if (currentValue["crops"] != null) + { + existingCropsArray = (JArray)currentValue["crops"]; + } + else + { + currentValue["crops"] = existingCropsArray = new JArray(); + } + + foreach (var preValueCrop in preValueCrops.Where(x => x.HasValues)) + { + var found = existingCropsArray.FirstOrDefault(x => + { + if (x.HasValues && x["alias"] != null) + { + return x["alias"].Value() == preValueCrop["alias"].Value(); + } + return false; + }); + if (found != null) + { + found["width"] = preValueCrop["width"]; + found["height"] = preValueCrop["height"]; + } + else + { + existingCropsArray.Add(preValueCrop); + } + } + } + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) return null; + var sourceString = source.ToString(); + + if (sourceString.DetectIsJson()) + { + JObject obj; + try + { + obj = JsonConvert.DeserializeObject(sourceString, new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }); + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the string " + sourceString + " to a json object", ex); + return sourceString; + } + + MergePreValues(obj, _dataTypeService, propertyType.DataTypeId); + + return obj; + } + + //it's not json, just return the string + return sourceString; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index e5ed8b8b94..c5f0236a1a 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters { var propertyEditor = PropertyEditorResolver.Current.GetByAlias(propertyType.PropertyEditorAlias); if (propertyEditor == null) return false; - return propertyEditor.ValueEditor.ValueType.InvariantEquals("json"); + return propertyEditor.ValueEditor.ValueType.InvariantEquals(PropertyEditorValueTypes.Json); } public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs new file mode 100644 index 0000000000..cf034c8c29 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -0,0 +1,28 @@ +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.PropertyEditors.ValueConverters +{ + /// + /// We need this property converter so that we always force the value of a label to be a string + /// + /// + /// Without a property converter defined for the label type, the value will be converted with + /// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this + /// can cause issues if the string is detected as a number and then strips leading zeros. + /// Example: http://issues.umbraco.org/issue/U4-7929 + /// + [DefaultPropertyValueConverter] + [PropertyValueType(typeof (string))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class LabelValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return Constants.PropertyEditors.NoEditAlias.Equals(propertyType.PropertyEditorAlias); + } + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + return source == null ? string.Empty : source.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs index 6725257888..39bcf85b12 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultipleTextStringValueConverter.cs @@ -35,6 +35,9 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // as xml in the database, it's always been new line delimited. Will ask Stephen about this. // In the meantime, we'll do this xml check, see if it parses and if not just continue with // splitting by newline + // + // RS: SD/Stephan Please consider post before deciding to remove + //// https://our.umbraco.org/forum/contributing-to-umbraco-cms/76989-keep-the-xml-values-in-the-multipletextstringvalueconverter var values = new List(); var pos = sourceString.IndexOf("", StringComparison.Ordinal); while (pos >= 0) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index e332cb6dca..524663c9c5 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -15,7 +16,6 @@ using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; -using Microsoft.Owin; using Umbraco.Core.Logging; namespace Umbraco.Core.Security @@ -103,10 +103,18 @@ namespace Umbraco.Core.Security var backOfficeIdentity = http.User.Identity as UmbracoBackOfficeIdentity; if (backOfficeIdentity != null) return backOfficeIdentity; + //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + var claimsPrincipal = http.User as ClaimsPrincipal; + if (claimsPrincipal != null) + { + backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); + if (backOfficeIdentity != null) return backOfficeIdentity; + } + //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session var claimsIdentity = http.User.Identity as ClaimsIdentity; if (claimsIdentity != null && claimsIdentity.IsAuthenticated) - { + { try { return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); @@ -157,9 +165,6 @@ namespace Umbraco.Core.Security return new HttpContextWrapper(http).GetCurrentIdentity(authenticateRequestIfNotFound); } - /// - /// This clears the forms authentication cookie - /// public static void UmbracoLogout(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); @@ -170,75 +175,18 @@ namespace Umbraco.Core.Security /// This clears the forms authentication cookie for webapi since cookies are handled differently /// /// + [Obsolete("Use OWIN IAuthenticationManager.SignOut instead", true)] + [EditorBrowsable(EditorBrowsableState.Never)] public static void UmbracoLogoutWebApi(this HttpResponseMessage response) { - if (response == null) throw new ArgumentNullException("response"); - //remove the cookie - var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - //remove the preview cookie too - var prevCookie = new CookieHeaderValue(Constants.Web.PreviewCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - //remove the external login cookie too - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - - response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); + throw new NotSupportedException("This method is not supported and should not be used, it has been removed in Umbraco 7.4"); } - /// - /// This adds the forms authentication cookie for webapi since cookies are handled differently - /// - /// - /// + [Obsolete("Use WebSecurity.SetPrincipalForRequest", true)] + [EditorBrowsable(EditorBrowsableState.Never)] public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) { - if (response == null) throw new ArgumentNullException("response"); - - //remove the external login cookie - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") - { - Expires = DateTime.Now.AddYears(-1), - Path = "/" - }; - - var userDataString = JsonConvert.SerializeObject(Mapper.Map(user)); - - var ticket = new FormsAuthenticationTicket( - 4, - user.Username, - DateTime.Now, - DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), - true, - userDataString, - "/" - ); - - // Encrypt the cookie using the machine key for secure transport - var encrypted = FormsAuthentication.Encrypt(ticket); - - //add the cookie - var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, encrypted) - { - //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way - Expires = DateTime.Now.AddMinutes(1440), - Path = "/", - Secure = GlobalSettings.UseSSL, - HttpOnly = true - }; - - response.Headers.AddCookies(new[] { authCookie, extLoginCookie }); - - return ticket; + throw new NotSupportedException("This method is not supported and should not be used, it has been removed in Umbraco 7.4"); } /// @@ -250,26 +198,29 @@ namespace Umbraco.Core.Security if (http == null) throw new ArgumentNullException("http"); new HttpContextWrapper(http).UmbracoLogout(); } - + /// - /// Renews the Umbraco authentication ticket + /// This will force ticket renewal in the OWIN pipeline /// /// /// public static bool RenewUmbracoAuthTicket(this HttpContextBase http) { if (http == null) throw new ArgumentNullException("http"); - return RenewAuthTicket(http, - UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, - UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain, - //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way - 1440); + http.Items["umbraco-force-auth"] = true; + return true; } + /// + /// This will force ticket renewal in the OWIN pipeline + /// + /// + /// internal static bool RenewUmbracoAuthTicket(this HttpContext http) { if (http == null) throw new ArgumentNullException("http"); - return new HttpContextWrapper(http).RenewUmbracoAuthTicket(); + http.Items["umbraco-force-auth"] = true; + return true; } /// @@ -358,7 +309,9 @@ namespace Umbraco.Core.Security { //TODO: Do we need to do more here?? need to make sure that the forms cookie is gone, but is that // taken care of in our custom middleware somehow? - ctx.Authentication.SignOut(); + ctx.Authentication.SignOut( + Core.Constants.Security.BackOfficeAuthenticationType, + Core.Constants.Security.BackOfficeExternalAuthenticationType); return null; } } @@ -390,8 +343,7 @@ namespace Umbraco.Core.Security //ensure there's def an expired cookie http.Response.Cookies.Add(new HttpCookie(c) { Expires = DateTime.Now.AddYears(-1) }); } - } - + } } private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) @@ -432,51 +384,6 @@ namespace Umbraco.Core.Security return FormsAuthentication.Decrypt(formsCookie); } - /// - /// Renews the forms authentication ticket & cookie - /// - /// - /// - /// - /// - /// true if there was a ticket to renew otherwise false if there was no ticket - private static bool RenewAuthTicket(this HttpContextBase http, string cookieName, string cookieDomain, int minutesPersisted) - { - if (http == null) throw new ArgumentNullException("http"); - //get the ticket - var ticket = GetAuthTicket(http, cookieName); - //renew the ticket - var renewed = FormsAuthentication.RenewTicketIfOld(ticket); - if (renewed == null) - { - return false; - } - - //get the request cookie to get it's expiry date, - //NOTE: this will never be null becaues we already do this - // check in teh GetAuthTicket. - var formsCookie = http.Request.Cookies[cookieName]; - - //encrypt it - var hash = FormsAuthentication.Encrypt(renewed); - //write it to the response - var cookie = new HttpCookie(cookieName, hash) - { - Expires = DateTime.Now.AddMinutes(minutesPersisted), - Domain = cookieDomain - }; - - if (GlobalSettings.UseSSL) - cookie.Secure = true; - - //ensure http only, this should only be able to be accessed via the server - cookie.HttpOnly = true; - - //rewrite the cooke - http.Response.Cookies.Set(cookie); - return true; - } - /// /// Creates a custom FormsAuthentication ticket with the data specified /// diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 8529132bb5..9c46ae69f4 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -9,6 +9,12 @@ namespace Umbraco.Core.Security { public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory { + public BackOfficeClaimsIdentityFactory() + { + SecurityStampClaimType = Constants.Security.SessionIdClaimType; + UserNameClaimType = ClaimTypes.Name; + } + /// /// Create a ClaimsIdentity from a user /// @@ -20,7 +26,7 @@ namespace Umbraco.Core.Security var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, //set a new session id - new UserData(Guid.NewGuid().ToString("N")) + new UserData { Id = user.Id, Username = user.UserName, @@ -29,7 +35,8 @@ namespace Umbraco.Core.Security Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId + StartMediaNode = user.StartMediaId, + SessionId = user.SecurityStamp }); return umbracoIdentity; diff --git a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs index 5247ea0af4..086d1a77bf 100644 --- a/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs +++ b/src/Umbraco.Core/Security/BackOfficeCookieAuthenticationProvider.cs @@ -1,12 +1,38 @@ +using System; using System.Globalization; +using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { public class BackOfficeCookieAuthenticationProvider : CookieAuthenticationProvider { + public override void ResponseSignOut(CookieResponseSignOutContext context) + { + base.ResponseSignOut(context); + + //Make sure the definitely all of these cookies are cleared when signing out with cookies + context.Response.Cookies.Append(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, "", new CookieOptions + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }); + context.Response.Cookies.Append(Constants.Web.PreviewCookieName, "", new CookieOptions + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }); + context.Response.Cookies.Append(Constants.Security.BackOfficeExternalCookieName, "", new CookieOptions + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }); + } + /// /// Ensures that the culture is set correctly for the current back office user /// diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index 85d6a0c715..f1d18b9d0f 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -53,6 +53,11 @@ namespace Umbraco.Core.Security switch (result) { case SignInStatus.Success: + _logger.WriteCore(TraceEventType.Information, 0, + string.Format( + "User: {0} logged in from IP address {1}", + userName, + _request.RemoteIpAddress), null, null); break; case SignInStatus.LockedOut: _logger.WriteCore(TraceEventType.Information, 0, diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index f86c06c39c..e48079c10b 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Text; +using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -9,8 +10,6 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { - - /// /// Default back office user manager /// @@ -21,6 +20,18 @@ namespace Umbraco.Core.Security { } + public BackOfficeUserManager( + IUserStore store, + IdentityFactoryOptions options, + MembershipProviderBase membershipProvider) + : base(store) + { + if (options == null) throw new ArgumentNullException("options"); + var manager = new BackOfficeUserManager(store); + InitUserManager(manager, membershipProvider, options); + } + + #region Static Create methods /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// @@ -40,8 +51,8 @@ namespace Umbraco.Core.Security if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, externalLoginService, membershipProvider)); - - return InitUserManager(manager, membershipProvider, options); + manager.InitUserManager(manager, membershipProvider, options); + return manager; } /// @@ -56,13 +67,10 @@ namespace Umbraco.Core.Security BackOfficeUserStore customUserStore, MembershipProviderBase membershipProvider) { - if (options == null) throw new ArgumentNullException("options"); - if (customUserStore == null) throw new ArgumentNullException("customUserStore"); - - var manager = new BackOfficeUserManager(customUserStore); - - return InitUserManager(manager, membershipProvider, options); - } + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); + return manager; + } + #endregion /// /// Initializes the user manager with the correct options @@ -71,7 +79,7 @@ namespace Umbraco.Core.Security /// /// /// - private static BackOfficeUserManager InitUserManager(BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions options) + protected void InitUserManager(BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions options) { // Configure validation logic for usernames manager.UserValidator = new UserValidator(manager) @@ -110,6 +118,8 @@ namespace Umbraco.Core.Security //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + manager.EmailService = new EmailService(); + //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user @@ -124,11 +134,10 @@ namespace Umbraco.Core.Security // BodyFormat = "Your security code is: {0}" //}); - //manager.EmailService = new EmailService(); - //manager.SmsService = new SmsService(); - - return manager; + //manager.SmsService = new SmsService(); } + + } /// @@ -171,5 +180,44 @@ namespace Umbraco.Core.Security } #endregion + /// + /// Logic used to validate a username and password + /// + /// + /// + /// + /// + /// By default this uses the standard ASP.Net Identity approach which is: + /// * Get password store + /// * Call VerifyPasswordAsync with the password store + user + password + /// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password + /// + /// In some cases people want simple custom control over the username/password check, for simplicity + /// sake, developers would like the users to simply validate against an LDAP directory but the user + /// data remains stored inside of Umbraco. + /// See: http://issues.umbraco.org/issue/U4-7032 for the use cases. + /// + /// We've allowed this check to be overridden with a simple callback so that developers don't actually + /// have to implement/override this class. + /// + public async override Task CheckPasswordAsync(T user, string password) + { + if (BackOfficeUserPasswordChecker != null) + { + var result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password); + //if the result indicates to not fallback to the default, then return true if the credentials are valid + if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker) + { + return result == BackOfficeUserPasswordCheckerResult.ValidCredentials; + } + } + //use the default behavior + return await base.CheckPasswordAsync(user, password); + } + + /// + /// Gets/sets the default back office user password checker + /// + public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } } } diff --git a/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs new file mode 100644 index 0000000000..8ce843856a --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserPasswordCheckerResult.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Security +{ + /// + /// The result returned from the IBackOfficeUserPasswordChecker + /// + public enum BackOfficeUserPasswordCheckerResult + { + ValidCredentials, + InvalidCredentials, + FallbackToDefaultChecker + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index e37f9d1a54..c6714e256a 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Data; +using System.Data.Common; using System.Linq; using System.Threading.Tasks; using System.Web.Security; @@ -97,6 +99,8 @@ namespace Umbraco.Core.Security } _userService.Save(member); + if (member.Id == 0) throw new DataException("Could not create the user, check logs for details"); + //re-assign id user.Id = member.Id; diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs new file mode 100644 index 0000000000..93864aa37c --- /dev/null +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -0,0 +1,26 @@ +using System.Net.Mail; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + public class EmailService : IIdentityMessageService + { + public async Task SendAsync(IdentityMessage message) + { + using (var client = new SmtpClient()) + using (var mailMessage = new MailMessage()) + { + mailMessage.Body = message.Body; + mailMessage.To.Add(message.Destination); + mailMessage.Subject = message.Subject; + + //TODO: This check could be nicer but that is the way it is currently + mailMessage.IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false + && message.Body.Contains("<") && message.Body.Contains("/>"); + + await client.SendMailAsync(mailMessage); + } + } + } +} diff --git a/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs new file mode 100644 index 0000000000..6ee65e0fef --- /dev/null +++ b/src/Umbraco.Core/Security/IBackOfficeUserPasswordChecker.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// Used by the BackOfficeUserManager to check the username/password which allows for developers to more easily + /// set the logic for this procedure. + /// + public interface IBackOfficeUserPasswordChecker + { + Task CheckPasswordAsync(BackOfficeIdentityUser user, string password); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 392c1545d1..3e02d6aec2 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -759,12 +759,13 @@ namespace Umbraco.Core.Security //This is the correct way to implement this (as per the sql membership provider) - switch ((int)PasswordFormat) + switch (PasswordFormat) { - case 0: + case MembershipPasswordFormat.Clear: return pass; - case 1: + case MembershipPasswordFormat.Hashed: throw new ProviderException("Provider can not decrypt hashed password"); + case MembershipPasswordFormat.Encrypted: default: var bytes = DecryptPassword(Convert.FromBase64String(pass)); return bytes == null ? null : Encoding.Unicode.GetString(bytes, 16, bytes.Length - 16); diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 6ce81f3e9f..1bc9902da5 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -209,17 +209,19 @@ namespace Umbraco.Core.Security if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this)); - - ////TODO: Not sure why this is null sometimes, it shouldn't be. Somewhere it's not being set - /// I think it's due to some bug I had in chrome, we'll see - //if (UserData.SessionId.IsNullOrWhiteSpace()) - //{ - // UserData.SessionId = Guid.NewGuid().ToString(); - //} - if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false) + if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false && SessionId.IsNullOrWhiteSpace() == false) + { AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + //The security stamp claim is also required... this is because this claim type is hard coded + // by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444 + if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false) + { + AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + //Add each app as a separate claim if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false) { diff --git a/src/Umbraco.Core/Security/UserData.cs b/src/Umbraco.Core/Security/UserData.cs index ff49636217..407d2782dd 100644 --- a/src/Umbraco.Core/Security/UserData.cs +++ b/src/Umbraco.Core/Security/UserData.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Security /// Use this constructor to create/assign new UserData to the ticket /// /// - /// A unique id that is assigned to this ticket + /// The security stamp for the user /// public UserData(string sessionId) { @@ -30,8 +30,7 @@ namespace Umbraco.Core.Security } /// - /// This is used to Id the current ticket which we can then use to mitigate csrf attacks - /// and other things that require request validation. + /// This is the 'security stamp' for validation /// [DataMember(Name = "sessionId")] public string SessionId { get; set; } @@ -42,8 +41,6 @@ namespace Umbraco.Core.Security [DataMember(Name = "roles")] public string[] Roles { get; set; } - //public int SessionTimeout { get; set; } - [DataMember(Name = "username")] public string Username { get; set; } diff --git a/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs new file mode 100644 index 0000000000..78dda6c4c6 --- /dev/null +++ b/src/Umbraco.Core/Serialization/NoTypeConverterJsonConverter.cs @@ -0,0 +1,52 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Umbraco.Core.Serialization +{ + /// + /// This is required if we want to force JSON.Net to not use .Net TypeConverters during serialization/deserialization + /// + /// + /// + /// In some cases thsi is required if your model has an explicit type converter, see: http://stackoverflow.com/a/31328131/694494 + /// + /// NOTE: I was going to use this for the ImageCropDataSetConverter to convert to String, which would have worked by putting this attribute: + /// [JsonConverter(typeof(NoTypeConverterJsonConverter{ImageCropDataSet}))] on top of the ImageCropDataSet class, however it turns out we + /// don't require this because to convert to string, we just override ToString(). + /// I'll leave this class here for the future though. + /// + internal class NoTypeConverterJsonConverter : JsonConverter + { + static readonly IContractResolver resolver = new NoTypeConverterContractResolver(); + + private class NoTypeConverterContractResolver : DefaultContractResolver + { + protected override JsonContract CreateContract(Type objectType) + { + if (typeof(T).IsAssignableFrom(objectType)) + { + var contract = this.CreateObjectContract(objectType); + contract.Converter = null; // Also null out the converter to prevent infinite recursion. + return contract; + } + return base.CreateContract(objectType); + } + } + + public override bool CanConvert(Type objectType) + { + return typeof(T).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Deserialize(reader, objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = resolver }).Serialize(writer, value); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index f640fa8d22..a0a5d8cb82 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -8,7 +8,6 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Cache; using File = System.IO.File; namespace Umbraco.Core.Services @@ -84,7 +83,7 @@ namespace Umbraco.Core.Services // based on the ApplicationTreeRegistrar - and as noted there this is not an ideal way to do things but were stuck like this // currently because of the legacy assemblies and types not in the Core. - //Get all the trees not registered in the config + //Get all the trees not registered in the config (those not matching by alias casing will be detected as "unregistered") var unregistered = _allAvailableTrees .Where(x => list.Any(l => l.Alias == x.Alias) == false) .ToArray(); @@ -93,19 +92,46 @@ namespace Umbraco.Core.Services if (hasChanges == false) return false; - //add the unregistered ones to the list and re-save the file if any changes were found + //add or edit the unregistered ones and re-save the file if any changes were found var count = 0; foreach (var tree in unregistered) { - doc.Root.Add(new XElement("add", - new XAttribute("initialize", tree.Initialize), - new XAttribute("sortOrder", tree.SortOrder), - new XAttribute("alias", tree.Alias), - new XAttribute("application", tree.ApplicationAlias), - new XAttribute("title", tree.Title), - new XAttribute("iconClosed", tree.IconClosed), - new XAttribute("iconOpen", tree.IconOpened), - new XAttribute("type", tree.Type))); + var existingElement = doc.Root.Elements("add").SingleOrDefault(x => + string.Equals(x.Attribute("alias").Value, tree.Alias, + StringComparison.InvariantCultureIgnoreCase) && + string.Equals(x.Attribute("application").Value, tree.ApplicationAlias, + StringComparison.InvariantCultureIgnoreCase)); + if (existingElement != null) + { + existingElement.SetAttributeValue("alias", tree.Alias); + } + else + { + if (tree.Title.IsNullOrWhiteSpace()) + { + doc.Root.Add(new XElement("add", + new XAttribute("initialize", tree.Initialize), + new XAttribute("sortOrder", tree.SortOrder), + new XAttribute("alias", tree.Alias), + new XAttribute("application", tree.ApplicationAlias), + new XAttribute("iconClosed", tree.IconClosed), + new XAttribute("iconOpen", tree.IconOpened), + new XAttribute("type", tree.Type))); + } + else + { + doc.Root.Add(new XElement("add", + new XAttribute("initialize", tree.Initialize), + new XAttribute("sortOrder", tree.SortOrder), + new XAttribute("alias", tree.Alias), + new XAttribute("application", tree.ApplicationAlias), + new XAttribute("title", tree.Title), + new XAttribute("iconClosed", tree.IconClosed), + new XAttribute("iconOpen", tree.IconOpened), + new XAttribute("type", tree.Type))); + } + + } count++; } @@ -124,11 +150,7 @@ namespace Umbraco.Core.Services } } } - - return list; - - }, new TimeSpan(0, 10, 0)); } @@ -315,7 +337,7 @@ namespace Umbraco.Core.Services //remove the cache now that it has changed SD: I'm leaving this here even though it // is taken care of by events as well, I think unit tests may rely on it being cleared here. - _cache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); + _cache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); } } } @@ -351,13 +373,15 @@ namespace Umbraco.Core.Services { list.Add(new ApplicationTree( addElement.Attribute("initialize") == null || Convert.ToBoolean(addElement.Attribute("initialize").Value), - addElement.Attribute("sortOrder") != null ? Convert.ToByte(addElement.Attribute("sortOrder").Value) : (byte)0, - addElement.Attribute("application").Value, - addElement.Attribute("alias").Value, - addElement.Attribute("title").Value, - addElement.Attribute("iconClosed").Value, - addElement.Attribute("iconOpen").Value, - addElement.Attribute("type").Value)); + addElement.Attribute("sortOrder") != null + ? Convert.ToByte(addElement.Attribute("sortOrder").Value) + : (byte)0, + (string)addElement.Attribute("application"), + (string)addElement.Attribute("alias"), + (string)addElement.Attribute("title"), + (string)addElement.Attribute("iconClosed"), + (string)addElement.Attribute("iconOpen"), + (string)addElement.Attribute("type"))); } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index da25bd0938..bbae07eb6a 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -152,6 +152,9 @@ namespace Umbraco.Core.Services { var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parentId, contentType); + var parent = GetById(content.ParentId); + content.Path = string.Concat(parent.IfNotNull(x => x.Path, content.ParentId.ToString()), ",", content.Id); + if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parentId), this)) { @@ -190,8 +193,11 @@ namespace Umbraco.Core.Services /// public IContent CreateContent(string name, IContent parent, string contentTypeAlias, int userId = 0) { + if (parent == null) throw new ArgumentNullException("parent"); + var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); + content.Path = string.Concat(parent.Path, ",", content.Id); if (Creating.IsRaisedEventCancelled(new NewEventArgs(content, contentTypeAlias, parent), this)) { @@ -276,6 +282,8 @@ namespace Umbraco.Core.Services /// public IContent CreateContentWithIdentity(string name, IContent parent, string contentTypeAlias, int userId = 0) { + if (parent == null) throw new ArgumentNullException("parent"); + var contentType = FindContentTypeByAlias(contentTypeAlias); var content = new Content(name, parent, contentType); @@ -445,6 +453,9 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetAncestors(IContent content) { + //null check otherwise we get exceptions + if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); + var ids = content.Path.Split(',').Where(x => x != Constants.System.Root.ToInvariantString() && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(int.Parse).ToArray(); if (ids.Any() == false) return new List(); @@ -476,22 +487,10 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.ParentId == id); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } + long total; + var result = GetPagedChildren(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; } /// @@ -507,6 +506,24 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, string filter = "") + { + return GetPagedChildren(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Children from + /// Page index (zero based) + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedChildren(int id, long pageIndex, int pageSize, out long totalChildren, + string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -519,7 +536,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.ParentId == id); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } @@ -529,22 +546,10 @@ namespace Umbraco.Core.Services [EditorBrowsable(EditorBrowsableState.Never)] public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); - Mandate.ParameterCondition(pageSize > 0, "pageSize"); - using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) - { - - var query = Query.Builder; - //if the id is System Root, then just get all - if (id != Constants.System.Root) - { - query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); - } - long total; - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out total, orderBy, orderDirection, filter); - totalChildren = Convert.ToInt32(total); - return contents; - } + long total; + var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); + totalChildren = Convert.ToInt32(total); + return result; } /// @@ -557,8 +562,25 @@ namespace Umbraco.Core.Services /// Field to order by /// Direction to order by /// Search text filter - /// An Enumerable list of objects + /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + { + return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); + } + + /// + /// Gets a collection of objects by Parent Id + /// + /// Id of the Parent to retrieve Descendants from + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy, Direction orderDirection, bool orderBySystemField, string filter) { Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); @@ -571,7 +593,7 @@ namespace Umbraco.Core.Services { query.Where(x => x.Path.SqlContains(string.Format(",{0},", id), TextColumnType.NVarchar)); } - var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, filter); + var contents = repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, orderBySystemField, filter); return contents; } @@ -895,11 +917,11 @@ namespace Umbraco.Core.Services { var originalPath = content.Path; - if (Trashing.IsRaisedEventCancelled( + if (Trashing.IsRaisedEventCancelled( new MoveEventArgs(evtMsgs, new MoveEventInfo(content, originalPath, Constants.System.RecycleBinContent)), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var moveInfo = new List> @@ -946,7 +968,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -1011,7 +1033,7 @@ namespace Umbraco.Core.Services /// True if unpublishing succeeded, otherwise False public bool UnPublish(IContent content, int userId = 0) { - return ((IContentServiceOperations) this).UnPublish(content, userId).Success; + return ((IContentServiceOperations)this).UnPublish(content, userId).Success; } /// @@ -1069,7 +1091,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(asArray, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } using (new WriteLock(Locker)) @@ -1113,7 +1135,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -1132,11 +1154,11 @@ namespace Umbraco.Core.Services using (new WriteLock(Locker)) { - if (Deleting.IsRaisedEventCancelled( + if (Deleting.IsRaisedEventCancelled( new DeleteEventArgs(content, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } //Make sure that published content is unpublished before being deleted @@ -1157,17 +1179,17 @@ namespace Umbraco.Core.Services { repository.Delete(content); uow.Commit(); - + var args = new DeleteEventArgs(content, false, evtMsgs); Deleted.RaiseEvent(args, this); - + //remove any flagged media files repository.DeleteMediaFiles(args.MediaFilesToDelete); } Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -1216,6 +1238,13 @@ namespace Umbraco.Core.Services /// Optional Id of the user issueing the delete operation public void DeleteContentOfType(int contentTypeId, int userId = 0) { + //TODO: This currently this is called from the ContentTypeService but that needs to change, + // if we are deleting a content type, we should just delete the data and do this operation slightly differently. + // This method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + using (new WriteLock(Locker)) { using (var uow = UowProvider.GetUnitOfWork()) @@ -1332,7 +1361,7 @@ namespace Umbraco.Core.Services /// Optional Id of the User deleting the Content public void MoveToRecycleBin(IContent content, int userId = 0) { - ((IContentServiceOperations) this).MoveToRecycleBin(content, userId); + ((IContentServiceOperations)this).MoveToRecycleBin(content, userId); } /// @@ -1636,7 +1665,7 @@ namespace Umbraco.Core.Services //TODO: This should not be an inner operation, but if we do this, it cannot raise events and cannot be cancellable! _publishingStrategy.PublishingFinalized(shouldBePublished, false); } - + Audit(AuditType.Sort, "Sorting content performed by user", userId, 0); @@ -1698,6 +1727,7 @@ namespace Umbraco.Core.Services } } + //TODO: All of this needs to be moved to the repository private void PerformMove(IContent content, int parentId, int userId, ICollection> moveInfo) { //add a tracking item to use in the Moved event @@ -1883,13 +1913,13 @@ namespace Umbraco.Core.Services content = newest; var evtMsgs = EventMessagesFactory.Get(); - + var published = content.Published ? content : GetPublishedVersion(content.Id); // get the published version if (published == null) { return Attempt.Succeed(new UnPublishStatus(content, UnPublishedStatusType.SuccessAlreadyUnPublished, evtMsgs)); // already unpublished } - + var unpublished = _publishingStrategy.UnPublish(content, userId); if (unpublished == false) return Attempt.Fail(new UnPublishStatus(content, UnPublishedStatusType.FailedCancelledByEvent, evtMsgs)); @@ -1966,6 +1996,10 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed if (content.HasIdentity == false) { @@ -2023,11 +2057,11 @@ namespace Umbraco.Core.Services if (raiseEvents) { - if (Saving.IsRaisedEventCancelled( + if (Saving.IsRaisedEventCancelled( new SaveEventArgs(content, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } } @@ -2059,7 +2093,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } } @@ -2108,6 +2142,22 @@ namespace Umbraco.Core.Services content.Name, content.Id)); return PublishStatusType.FailedPathNotPublished; } + else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } return PublishStatusType.Success; } diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index f144fa51e4..34dcdc01a9 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using System.Threading; +using AutoMapper; using Umbraco.Core.Auditing; using Umbraco.Core.Configuration; using Umbraco.Core.Events; @@ -28,7 +29,7 @@ namespace Umbraco.Core.Services private readonly IContentService _contentService; private readonly IMediaService _mediaService; - //Support recursive locks because some of the methods that require locking call other methods that require locking. + //Support recursive locks because some of the methods that require locking call other methods that require locking. //for example, the Move method needs to be locked but this calls the Save method which also needs to be locked. private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); @@ -41,6 +42,298 @@ namespace Umbraco.Core.Services _mediaService = mediaService; } + #region Containers + + public Attempt> CreateContentTypeContainer(int parentId, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + try + { + var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) + { + Name = name, + ParentId = parentId, + CreatorId = userId + }; + + if (SavingContentTypeContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.AddOrUpdate(container); + uow.Commit(); + + SavedContentTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + //TODO: Audit trail ? + + return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); + } + catch (Exception ex) + { + return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); + } + } + } + + public Attempt> CreateMediaTypeContainer(int parentId, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + { + try + { + var container = new EntityContainer(Constants.ObjectTypes.MediaTypeGuid) + { + Name = name, + ParentId = parentId, + CreatorId = userId + }; + + if (SavingMediaTypeContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.AddOrUpdate(container); + uow.Commit(); + + SavedMediaTypeContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + //TODO: Audit trail ? + + return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); + } + catch (Exception ex) + { + return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); + } + } + } + + public Attempt SaveContentTypeContainer(EntityContainer container, int userId = 0) + { + return SaveContainer( + SavingContentTypeContainer, SavedContentTypeContainer, + container, Constants.ObjectTypes.DocumentTypeContainerGuid, "document type", userId); + } + + public Attempt SaveMediaTypeContainer(EntityContainer container, int userId = 0) + { + return SaveContainer( + SavingMediaTypeContainer, SavedMediaTypeContainer, + container, Constants.ObjectTypes.MediaTypeContainerGuid, "media type", userId); + } + + private Attempt SaveContainer( + TypedEventHandler> savingEvent, + TypedEventHandler> savedEvent, + EntityContainer container, + Guid containerObjectType, + string objectTypeName, int userId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (container.ContainedObjectType != containerObjectType) + { + var ex = new InvalidOperationException("Not a " + objectTypeName + " container."); + return OperationStatus.Exception(evtMsgs, ex); + } + + if (container.HasIdentity && container.IsPropertyDirty("ParentId")) + { + var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + return OperationStatus.Exception(evtMsgs, ex); + } + + if (savingEvent.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) + { + repo.AddOrUpdate(container); + uow.Commit(); + } + + savedEvent.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + + //TODO: Audit trail ? + + return OperationStatus.Success(evtMsgs); + } + + public EntityContainer GetContentTypeContainer(int containerId) + { + return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); + } + + public EntityContainer GetMediaTypeContainer(int containerId) + { + return GetContainer(containerId, Constants.ObjectTypes.MediaTypeContainerGuid); + } + + private EntityContainer GetContainer(int containerId, Guid containerObjectType) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) + { + var container = repo.Get(containerId); + return container; + } + } + + public IEnumerable GetMediaTypeContainers(int[] containerIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + { + return repo.GetAll(containerIds); + } + } + + public IEnumerable GetMediaTypeContainers(string name, int level) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + { + return repo.Get(name, level); + } + } + + public IEnumerable GetMediaTypeContainers(IMediaType mediaType) + { + var ancestorIds = mediaType.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + if (asInt) return asInt.Result; + return int.MinValue; + }) + .Where(x => x != int.MinValue && x != mediaType.Id) + .ToArray(); + + return GetMediaTypeContainers(ancestorIds); + } + + public EntityContainer GetContentTypeContainer(Guid containerId) + { + return GetContainer(containerId, Constants.ObjectTypes.DocumentTypeContainerGuid); + } + + public IEnumerable GetContentTypeContainers(int[] containerIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + return repo.GetAll(containerIds); + } + } + + public IEnumerable GetContentTypeContainers(IContentType contentType) + { + var ancestorIds = contentType.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + if (asInt) return asInt.Result; + return int.MinValue; + }) + .Where(x => x != int.MinValue && x != contentType.Id) + .ToArray(); + + return GetContentTypeContainers(ancestorIds); + } + + public EntityContainer GetMediaTypeContainer(Guid containerId) + { + return GetContainer(containerId, Constants.ObjectTypes.MediaTypeContainerGuid); + } + + private EntityContainer GetContainer(Guid containerId, Guid containerObjectType) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, containerObjectType)) + { + var container = repo.Get(containerId); + return container; + } + } + + public IEnumerable GetContentTypeContainers(string name, int level) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + return repo.Get(name, level); + } + } + + public Attempt DeleteContentTypeContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + var container = repo.Get(containerId); + if (container == null) return OperationStatus.NoOperation(evtMsgs); + + if (DeletingContentTypeContainer.IsRaisedEventCancelled( + new DeleteEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.Delete(container); + uow.Commit(); + + DeletedContentTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + + return OperationStatus.Success(evtMsgs); + //TODO: Audit trail ? + } + } + + public Attempt DeleteMediaTypeContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + { + var container = repo.Get(containerId); + if (container == null) return OperationStatus.NoOperation(evtMsgs); + + if (DeletingMediaTypeContainer.IsRaisedEventCancelled( + new DeleteEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.Delete(container); + uow.Commit(); + + DeletedMediaTypeContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + + return OperationStatus.Success(evtMsgs); + //TODO: Audit trail ? + } + } + + #endregion + /// /// Gets all property type aliases. /// @@ -53,6 +346,22 @@ namespace Umbraco.Core.Services } } + /// + /// Gets all content type aliases + /// + /// + /// If this list is empty, it will return all content type aliases for media, members and content, otherwise + /// it will only return content type aliases for the object types specified + /// + /// + public IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAllContentTypeAliases(objectTypes); + } + } + /// /// Copies a content type as a child under the specified parent if specified (otherwise to the root) /// @@ -71,7 +380,7 @@ namespace Umbraco.Core.Services /// public IContentType Copy(IContentType original, string alias, string name, int parentId = -1) { - IContentType parent = null; + IContentType parent = null; if (parentId > 0) { parent = GetContentType(parentId); @@ -105,7 +414,7 @@ namespace Umbraco.Core.Services Mandate.ParameterNotNullOrEmpty(alias, "alias"); if (parent != null) { - Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent content type must have an identity")); + Mandate.That(parent.HasIdentity, () => new InvalidOperationException("The parent content type must have an identity")); } var clone = original.DeepCloneWithResetIdentities(alias); @@ -131,7 +440,7 @@ namespace Umbraco.Core.Services //set to root clone.ParentId = -1; } - + Save(clone); return clone; } @@ -158,10 +467,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.Alias == alias); - var contentTypes = repository.GetByQuery(query); - - return contentTypes.FirstOrDefault(); + return repository.Get(alias); } } @@ -199,7 +505,7 @@ namespace Umbraco.Core.Services public IEnumerable GetAllContentTypes(IEnumerable ids) { using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) - { + { return repository.GetAll(ids.ToArray()); } } @@ -293,7 +599,7 @@ namespace Umbraco.Core.Services { //this should never occur, the content service should always be typed but we'll check anyways. _contentService.RePublishAll(); - } + } } else if (firstType is IMediaType) { @@ -302,21 +608,50 @@ namespace Umbraco.Core.Services if (typedContentService != null) { typedContentService.RebuildXmlStructures(toUpdate.Select(x => x.Id).ToArray()); - } + } } } - + } - public void Validate(IContentTypeComposition compo) + public int CountContentTypes() + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.Count(Query.Builder); + } + } + + public int CountMediaTypes() + { + using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.Count(Query.Builder); + } + } + + /// + /// Validates the composition, if its invalid a list of property type aliases that were duplicated is returned + /// + /// + /// + public Attempt ValidateComposition(IContentTypeComposition compo) { using (new WriteLock(Locker)) { - ValidateLocked(compo); + try + { + ValidateLocked(compo); + return Attempt.Succeed(); + } + catch (InvalidCompositionException ex) + { + return Attempt.Fail(ex.PropertyTypeAliases, ex); + } } } - private void ValidateLocked(IContentTypeComposition compositionContentType) + protected void ValidateLocked(IContentTypeComposition compositionContentType) { // performs business-level validation of the composition // should ensure that it is absolutely safe to save the composition @@ -326,14 +661,17 @@ namespace Umbraco.Core.Services var contentType = compositionContentType as IContentType; var mediaType = compositionContentType as IMediaType; + var memberType = compositionContentType as IMemberType; // should NOT do it here but... v8! IContentTypeComposition[] allContentTypes; if (contentType != null) allContentTypes = GetAllContentTypes().Cast().ToArray(); else if (mediaType != null) allContentTypes = GetAllMediaTypes().Cast().ToArray(); + else if (memberType != null) + return; // no compositions on members, always validate else - throw new Exception("Composition is neither IContentType nor IMediaType?"); + throw new Exception("Composition is neither IContentType nor IMediaType nor IMemberType?"); var compositionAliases = compositionContentType.CompositionAliases(); var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y))); @@ -349,7 +687,7 @@ namespace Umbraco.Core.Services dependencies.Add(indirectReference); //Get all compositions for the current indirect reference var directReferences = indirectReference.ContentTypeComposition; - + foreach (var directReference in directReferences) { if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue; @@ -370,9 +708,7 @@ namespace Umbraco.Core.Services var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); if (intersect.Length == 0) continue; - var message = string.Format("The following PropertyType aliases from the current ContentType conflict with existing PropertyType aliases: {0}.", - string.Join(", ", intersect)); - throw new Exception(message); + throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); } } @@ -383,7 +719,7 @@ namespace Umbraco.Core.Services /// Optional id of the user saving the ContentType public void Save(IContentType contentType, int userId = 0) { - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) + if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) return; using (new WriteLock(Locker)) @@ -413,7 +749,7 @@ namespace Umbraco.Core.Services { var asArray = contentTypes.ToArray(); - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) + if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(asArray), this)) return; using (new WriteLock(Locker)) @@ -449,12 +785,19 @@ namespace Umbraco.Core.Services /// Optional id of the user issueing the delete /// Deleting a will delete all the objects based on this public void Delete(IContentType contentType, int userId = 0) - { - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentType), this)) + { + if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(contentType), this)) return; using (new WriteLock(Locker)) { + + //TODO: This needs to change, if we are deleting a content type, we should just delete the data, + // this method will recursively go lookup every content item, check if any of it's descendants are + // of a different type, move them to the recycle bin, then permanently delete the content items. + // The main problem with this is that for every content item being deleted, events are raised... + // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. + _contentService.DeleteContentOfType(contentType.Id); var uow = UowProvider.GetUnitOfWork(); @@ -482,7 +825,7 @@ namespace Umbraco.Core.Services { var asArray = contentTypes.ToArray(); - if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) + if (DeletingContentType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) return; using (new WriteLock(Locker)) @@ -508,7 +851,7 @@ namespace Umbraco.Core.Services Audit(AuditType.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); } } - + /// /// Gets an object by its Id /// @@ -531,10 +874,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.Alias == alias); - var contentTypes = repository.GetByQuery(query); - - return contentTypes.FirstOrDefault(); + return repository.Get(alias); } } @@ -641,6 +981,178 @@ namespace Umbraco.Core.Services } } + public Attempt> MoveMediaType(IMediaType toMove, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (MovingMediaType.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), + this)) + { + return Attempt.Fail( + new OperationStatus( + MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var moveInfo = new List>(); + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + { + try + { + EntityContainer container = null; + if (containerId > 0) + { + container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + moveInfo.AddRange(repository.Move(toMove, container)); + } + catch (DataOperationException ex) + { + return Attempt.Fail( + new OperationStatus(ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + MovedMediaType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + return Attempt.Succeed( + new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + } + + public Attempt> MoveContentType(IContentType toMove, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (MovingContentType.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, containerId)), + this)) + { + return Attempt.Fail( + new OperationStatus( + MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var moveInfo = new List>(); + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + { + try + { + EntityContainer container = null; + if (containerId > 0) + { + container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + moveInfo.AddRange(repository.Move(toMove, container)); + } + catch (DataOperationException ex) + { + return Attempt.Fail( + new OperationStatus(ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + MovedContentType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + return Attempt.Succeed( + new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + } + + public Attempt> CopyMediaType(IMediaType toCopy, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + IMediaType copy; + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.MediaTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + { + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + var alias = repository.GetUniqueAlias(toCopy.Alias); + copy = toCopy.DeepCloneWithResetIdentities(alias); + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repository.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repository.AddOrUpdate(copy); + } + catch (DataOperationException ex) + { + return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + return Attempt.Succeed(new OperationStatus(copy, MoveOperationStatusType.Success, evtMsgs)); + } + + public Attempt> CopyContentType(IContentType toCopy, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + IContentType copy; + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + { + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + var alias = repository.GetUniqueAlias(toCopy.Alias); + copy = toCopy.DeepCloneWithResetIdentities(alias); + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repository.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repository.AddOrUpdate(copy); + } + catch (DataOperationException ex) + { + return Attempt.Fail(new OperationStatus(null, ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + return Attempt.Succeed(new OperationStatus(copy, MoveOperationStatusType.Success, evtMsgs)); + } + /// /// Saves a single object /// @@ -648,7 +1160,7 @@ namespace Umbraco.Core.Services /// Optional Id of the user saving the MediaType public void Save(IMediaType mediaType, int userId = 0) { - if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(mediaType), this)) + if (SavingMediaType.IsRaisedEventCancelled(new SaveEventArgs(mediaType), this)) return; using (new WriteLock(Locker)) @@ -660,7 +1172,7 @@ namespace Umbraco.Core.Services mediaType.CreatorId = userId; repository.AddOrUpdate(mediaType); uow.Commit(); - + } UpdateContentXmlStructure(mediaType); @@ -699,7 +1211,7 @@ namespace Umbraco.Core.Services } //save it all in one go - uow.Commit(); + uow.Commit(); } UpdateContentXmlStructure(asArray.Cast().ToArray()); @@ -717,7 +1229,7 @@ namespace Umbraco.Core.Services /// Deleting a will delete all the objects based on this public void Delete(IMediaType mediaType, int userId = 0) { - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaType), this)) + if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(mediaType), this)) return; using (new WriteLock(Locker)) { @@ -747,7 +1259,7 @@ namespace Umbraco.Core.Services { var asArray = mediaTypes.ToArray(); - if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) + if (DeletingMediaType.IsRaisedEventCancelled(new DeleteEventArgs(asArray), this)) return; using (new WriteLock(Locker)) { @@ -769,7 +1281,7 @@ namespace Umbraco.Core.Services } Audit(AuditType.Delete, string.Format("Delete MediaTypes performed by user"), userId, -1); - } + } } /// @@ -836,19 +1348,29 @@ namespace Umbraco.Core.Services uow.Commit(); } } - + #region Event Handlers - /// - /// Occurs before Delete - /// - public static event TypedEventHandler> DeletingContentType; + public static event TypedEventHandler> SavingContentTypeContainer; + public static event TypedEventHandler> SavedContentTypeContainer; + public static event TypedEventHandler> DeletingContentTypeContainer; + public static event TypedEventHandler> DeletedContentTypeContainer; + public static event TypedEventHandler> SavingMediaTypeContainer; + public static event TypedEventHandler> SavedMediaTypeContainer; + public static event TypedEventHandler> DeletingMediaTypeContainer; + public static event TypedEventHandler> DeletedMediaTypeContainer; + + + /// + /// Occurs before Delete + /// + public static event TypedEventHandler> DeletingContentType; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedContentType; - + /// /// Occurs before Delete /// @@ -858,7 +1380,7 @@ namespace Umbraco.Core.Services /// Occurs after Delete /// public static event TypedEventHandler> DeletedMediaType; - + /// /// Occurs before Save /// @@ -879,6 +1401,26 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> SavedMediaType; + /// + /// Occurs before Move + /// + public static event TypedEventHandler> MovingMediaType; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> MovedMediaType; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> MovingContentType; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> MovedContentType; + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs index c071c7bbba..df29012b90 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceBase.cs @@ -15,7 +15,7 @@ namespace Umbraco.Core.Services : base(provider, repositoryFactory, logger, eventMessagesFactory) { } - + /// /// This is called after an content type is saved and is used to update the content xml structures in the database /// if they are required to be updated. diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs new file mode 100644 index 0000000000..ed04edc6bf --- /dev/null +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services +{ + public static class ContentTypeServiceExtensions + { + /// + /// Returns the available composite content types for a given content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, + IContentTypeComposition source, + IContentTypeComposition[] allContentTypes, + string[] filterContentTypes = null, + string[] filterPropertyTypes = null) + { + filterContentTypes = filterContentTypes == null + ? new string[] { } + : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + + filterPropertyTypes = filterPropertyTypes == null + ? new string[] {} + : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + + //create the full list of property types to use as the filter + //this is the combination of all property type aliases found in the content types passed in for the filter + //as well as the specific property types passed in for the filter + filterPropertyTypes = allContentTypes + .Where(c => filterContentTypes.InvariantContains(c.Alias)) + .SelectMany(c => c.PropertyTypes) + .Select(c => c.Alias) + .Union(filterPropertyTypes) + .ToArray(); + + var sourceId = source != null ? source.Id : 0; + + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); + if (isUsing.Length > 0) + { + //if already in use a composition, do not allow any composited types + return new ContentTypeAvailableCompositionsResults(); + } + + // if it is not used then composition is possible + // hashset guarantees unicity on Id + var list = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + // usable types are those that are top-level + var usableContentTypes = allContentTypes + .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + foreach (var x in usableContentTypes) + list.Add(x); + + // indirect types are those that we use, directly or indirectly + var indirectContentTypes = GetDirectOrIndirect(source).ToArray(); + foreach (var x in indirectContentTypes) + list.Add(x); + + //At this point we have a list of content types that 'could' be compositions + + //now we'll filter this list based on the filters requested + var filtered = list + .Where(x => + { + //need to filter any content types that are included in this list + return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false; + }) + .Where(x => + { + //need to filter any content types that have matching property aliases that are included in this list + //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified + return filterPropertyTypes.Intersect( + x.PropertyTypes.Select(p => p.Alias), + StringComparer.InvariantCultureIgnoreCase).Any() == false; + }) + .OrderBy(x => x.Name) + .ToList(); + + //get ancestor ids - we will filter all ancestors + var ancestors = GetAncestors(source, allContentTypes); + var ancestorIds = ancestors.Select(x => x.Id).ToArray(); + + //now we can create our result based on what is still available and the ancestors + var result = list + //not itself + .Where(x => x.Id != sourceId) + .OrderBy(x => x.Name) + .Select(composition => filtered.Contains(composition) + ? new ContentTypeAvailableCompositionsResult(composition, ancestorIds.Contains(composition.Id) == false) + : new ContentTypeAvailableCompositionsResult(composition, false)).ToList(); + + return new ContentTypeAvailableCompositionsResults(ancestors, result); + } + + private static IContentTypeComposition[] GetAncestors(IContentTypeComposition ctype, IContentTypeComposition[] allContentTypes) + { + if (ctype == null) return new IContentTypeComposition[] {}; + var ancestors = new List(); + var parentId = ctype.ParentId; + while (parentId > 0) + { + var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); + if (parent != null) + { + ancestors.Add(parent); + parentId = parent.ParentId; + } + else + { + parentId = -1; + } + } + return ancestors.ToArray(); + } + + /// + /// Get those that we use directly + /// + /// + /// + private static IEnumerable GetDirectOrIndirect(IContentTypeComposition ctype) + { + if (ctype == null) return Enumerable.Empty(); + + // hashset guarantees unicity on Id + var all = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + var stack = new Stack(); + + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); + + while (stack.Count > 0) + { + var x = stack.Pop(); + all.Add(x); + foreach (var y in x.ContentTypeComposition) + stack.Push(y); + } + + return all; + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index f6dd53020c..2cdcfc76d5 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using umbraco.interfaces; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Services { @@ -26,6 +27,163 @@ namespace Umbraco.Core.Services { } + #region Containers + + public Attempt> CreateContainer(int parentId, string name, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + try + { + var container = new EntityContainer(Constants.ObjectTypes.DataTypeGuid) + { + Name = name, + ParentId = parentId, + CreatorId = userId + }; + + if (SavingContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(container, OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.AddOrUpdate(container); + uow.Commit(); + + SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + //TODO: Audit trail ? + + return Attempt.Succeed(new OperationStatus(container, OperationStatusType.Success, evtMsgs)); + } + catch (Exception ex) + { + return Attempt.Fail(new OperationStatus(null, OperationStatusType.FailedExceptionThrown, evtMsgs), ex); + } + } + } + + public EntityContainer GetContainer(int containerId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + var container = repo.Get(containerId); + return container; + } + } + + public EntityContainer GetContainer(Guid containerId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + var container = repo.Get(containerId); + return container; + } + } + + public IEnumerable GetContainers(string name, int level) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + return repo.Get(name, level); + } + } + + public IEnumerable GetContainers(IDataTypeDefinition dataTypeDefinition) + { + var ancestorIds = dataTypeDefinition.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => + { + var asInt = x.TryConvertTo(); + if (asInt) return asInt.Result; + return int.MinValue; + }) + .Where(x => x != int.MinValue && x != dataTypeDefinition.Id) + .ToArray(); + + return GetContainers(ancestorIds); + } + + public IEnumerable GetContainers(int[] containerIds) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + return repo.GetAll(containerIds); + } + } + + public Attempt SaveContainer(EntityContainer container, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (container.ContainedObjectType != Constants.ObjectTypes.DataTypeGuid) + { + var ex = new InvalidOperationException("Not a " + Constants.ObjectTypes.DataTypeGuid + " container."); + return OperationStatus.Exception(evtMsgs, ex); + } + + if (container.HasIdentity && container.IsPropertyDirty("ParentId")) + { + var ex = new InvalidOperationException("Cannot save a container with a modified parent, move the container instead."); + return OperationStatus.Exception(evtMsgs, ex); + } + + if (SavingContainer.IsRaisedEventCancelled( + new SaveEventArgs(container, evtMsgs), + this)) + { + return OperationStatus.Cancelled(evtMsgs); + } + + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + repo.AddOrUpdate(container); + uow.Commit(); + } + + SavedContainer.RaiseEvent(new SaveEventArgs(container, evtMsgs), this); + + //TODO: Audit trail ? + + return OperationStatus.Success(evtMsgs); + } + + public Attempt DeleteContainer(int containerId, int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + { + var container = repo.Get(containerId); + if (container == null) return OperationStatus.NoOperation(evtMsgs); + + if (DeletingContainer.IsRaisedEventCancelled( + new DeleteEventArgs(container, evtMsgs), + this)) + { + return Attempt.Fail(new OperationStatus(OperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + repo.Delete(container); + uow.Commit(); + + DeletedContainer.RaiseEvent(new DeleteEventArgs(container, evtMsgs), this); + + return OperationStatus.Success(evtMsgs); + //TODO: Audit trail ? + } + } + + #endregion + /// /// Gets a by its Name /// @@ -153,6 +311,49 @@ namespace Umbraco.Core.Services } } + public Attempt> Move(IDataTypeDefinition toMove, int parentId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (Moving.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)), + this)) + { + return Attempt.Fail( + new OperationStatus( + MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var moveInfo = new List>(); + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DataTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) + { + try + { + EntityContainer container = null; + if (parentId > 0) + { + container = containerRepository.Get(parentId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + moveInfo.AddRange(repository.Move(toMove, container)); + } + catch (DataOperationException ex) + { + return Attempt.Fail( + new OperationStatus(ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + Moved.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + return Attempt.Succeed( + new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + } + /// /// Saves an /// @@ -303,6 +504,10 @@ namespace Umbraco.Core.Services if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) return; + // if preValues contain the data type, override the data type definition accordingly + if (values != null && values.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) + dataTypeDefinition.DatabaseType = PropertyValueEditor.GetDatabaseType(values[Constants.PropertyEditors.PreValueKeys.DataValueType].Value); + var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) { @@ -322,7 +527,6 @@ namespace Umbraco.Core.Services Audit(AuditType.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); } - /// /// Deletes an /// @@ -382,6 +586,12 @@ namespace Umbraco.Core.Services } #region Event Handlers + + public static event TypedEventHandler> SavingContainer; + public static event TypedEventHandler> SavedContainer; + public static event TypedEventHandler> DeletingContainer; + public static event TypedEventHandler> DeletedContainer; + /// /// Occurs before Delete /// @@ -401,8 +611,18 @@ namespace Umbraco.Core.Services /// Occurs after Save /// public static event TypedEventHandler> Saved; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> Moving; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> Moved; #endregion - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/DomainService.cs b/src/Umbraco.Core/Services/DomainService.cs index ca9fe03dcb..3ffcb92778 100644 --- a/src/Umbraco.Core/Services/DomainService.cs +++ b/src/Umbraco.Core/Services/DomainService.cs @@ -33,7 +33,7 @@ namespace Umbraco.Core.Services new DeleteEventArgs(domain, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var uow = UowProvider.GetUnitOfWork(); @@ -45,7 +45,7 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(domain, false, evtMsgs); Deleted.RaiseEvent(args, this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } public IDomain GetByName(string name) @@ -91,7 +91,7 @@ namespace Umbraco.Core.Services new SaveEventArgs(domainEntity, evtMsgs), this)) { - return Attempt.Fail(OperationStatus.Cancelled(evtMsgs)); + return OperationStatus.Cancelled(evtMsgs); } var uow = UowProvider.GetUnitOfWork(); @@ -102,7 +102,7 @@ namespace Umbraco.Core.Services } Saved.RaiseEvent(new SaveEventArgs(domainEntity, false, evtMsgs), this); - return Attempt.Succeed(OperationStatus.Success(evtMsgs)); + return OperationStatus.Success(evtMsgs); } #region Event Handlers diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 78fe48ec76..257416d054 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Cache; using Umbraco.Core.CodeAnnotations; using Umbraco.Core.Events; @@ -35,7 +36,28 @@ namespace Umbraco.Core.Services {typeof (IMedia).FullName, new Tuple>(UmbracoObjectTypes.Media, mediaService.GetById)}, {typeof (IMediaType).FullName, new Tuple>(UmbracoObjectTypes.MediaType, contentTypeService1.GetMediaType)}, {typeof (IMember).FullName, new Tuple>(UmbracoObjectTypes.Member, memberService.GetById)}, - {typeof (IMemberType).FullName, new Tuple>(UmbracoObjectTypes.MemberType, memberTypeService.Get)} + {typeof (IMemberType).FullName, new Tuple>(UmbracoObjectTypes.MemberType, memberTypeService.Get)}, + //{typeof (IUmbracoEntity).FullName, new Tuple>(UmbracoObjectTypes.EntityContainer, id => + //{ + // using (var uow = UowProvider.GetUnitOfWork()) + // { + // var found = uow.Database.FirstOrDefault("SELECT * FROM umbracoNode WHERE id=@id", new { id = id }); + // return found == null ? null : new UmbracoEntity(found.Trashed) + // { + // Id = found.NodeId, + // Name = found.Text, + // Key = found.UniqueId, + // SortOrder = found.SortOrder, + // Path = found.Path, + // NodeObjectTypeId = found.NodeObjectType.Value, + // CreateDate = found.CreateDate, + // CreatorId = found.UserId.Value, + // Level = found.Level, + // ParentId = found.ParentId + // }; + // } + + //})} }; } @@ -62,6 +84,7 @@ namespace Umbraco.Core.Services case UmbracoObjectTypes.DocumentType: case UmbracoObjectTypes.Member: case UmbracoObjectTypes.DataType: + case UmbracoObjectTypes.DocumentTypeContainer: return uow.Database.ExecuteScalar(new Sql().Select("id").From().Where(dto => dto.UniqueId == key)); case UmbracoObjectTypes.RecycleBin: case UmbracoObjectTypes.Stylesheet: diff --git a/src/Umbraco.Core/Services/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/EntityXmlSerializer.cs index 772009e89a..1e7a95d6fd 100644 --- a/src/Umbraco.Core/Services/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/EntityXmlSerializer.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Web; using System.Xml; using System.Xml.Linq; using Umbraco.Core.Configuration; @@ -100,6 +102,8 @@ namespace Umbraco.Core.Services xml.Add(new XAttribute("loginName", member.Username)); xml.Add(new XAttribute("email", member.Email)); + + xml.Add(new XAttribute("icon", member.ContentType.Icon)); return xml; } @@ -160,6 +164,20 @@ namespace Umbraco.Core.Services xml.Add(new XAttribute("Definition", dataTypeDefinition.Key)); xml.Add(new XAttribute("DatabaseType", dataTypeDefinition.DatabaseType.ToString())); + var folderNames = string.Empty; + if (dataTypeDefinition.Level != 1) + { + //get url encoded folder names + var folders = dataTypeService.GetContainers(dataTypeDefinition) + .OrderBy(x => x.Level) + .Select(x => HttpUtility.UrlEncode(x.Name)); + + folderNames = string.Join("/", folders.ToArray()); + } + + if (string.IsNullOrWhiteSpace(folderNames) == false) + xml.Add(new XAttribute("Folders", folderNames)); + return xml; } @@ -249,11 +267,15 @@ namespace Umbraco.Core.Services structure.Add(new XElement("MediaType", allowedType.Alias)); } - var genericProperties = new XElement("GenericProperties"); + var genericProperties = new XElement("GenericProperties"); // actually, all of them foreach (var propertyType in mediaType.PropertyTypes) { var definition = dataTypeService.GetDataTypeDefinitionById(propertyType.DataTypeDefinitionId); - var propertyGroup = mediaType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); + + var propertyGroup = propertyType.PropertyGroupId == null // true generic property + ? null + : mediaType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); + var genericProperty = new XElement("GenericProperty", new XElement("Name", propertyType.Name), new XElement("Alias", propertyType.Alias), @@ -319,9 +341,10 @@ namespace Umbraco.Core.Services /// Exports an item to xml as an /// /// + /// /// Content type to export /// containing the xml representation of the IContentType object - public XElement Serialize(IDataTypeService dataTypeService, IContentType contentType) + public XElement Serialize(IDataTypeService dataTypeService, IContentTypeService contentTypeService, IContentType contentType) { var info = new XElement("Info", new XElement("Name", contentType.Name), @@ -362,14 +385,14 @@ namespace Umbraco.Core.Services structure.Add(new XElement("DocumentType", allowedType.Alias)); } - var genericProperties = new XElement("GenericProperties"); + var genericProperties = new XElement("GenericProperties"); // actually, all of them foreach (var propertyType in contentType.PropertyTypes) { var definition = dataTypeService.GetDataTypeDefinitionById(propertyType.DataTypeDefinitionId); - var propertyGroup = propertyType.PropertyGroupId == null - ? null - : contentType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); + var propertyGroup = propertyType.PropertyGroupId == null // true generic property + ? null + : contentType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value); var genericProperty = new XElement("GenericProperty", new XElement("Name", propertyType.Name), @@ -377,9 +400,11 @@ namespace Umbraco.Core.Services new XElement("Type", propertyType.PropertyEditorAlias), new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), + new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), - new XElement("Validation", propertyType.ValidationRegExp), - new XElement("Description", new XCData(propertyType.Description))); + propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, + propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + genericProperties.Add(genericProperty); } @@ -393,11 +418,28 @@ namespace Umbraco.Core.Services tabs.Add(tab); } - return new XElement("DocumentType", - info, - structure, - genericProperties, - tabs); + var xml = new XElement("DocumentType", + info, + structure, + genericProperties, + tabs); + + var folderNames = string.Empty; + //don't add folders if this is a child doc type + if (contentType.Level != 1 && masterContentType == null) + { + //get url encoded folder names + var folders = contentTypeService.GetContentTypeContainers(contentType) + .OrderBy(x => x.Level) + .Select(x => HttpUtility.UrlEncode(x.Name)); + + folderNames = string.Join("/", folders.ToArray()); + } + + if (string.IsNullOrWhiteSpace(folderNames) == false) + xml.Add(new XAttribute("Folders", folderNames)); + + return xml; } /// diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index aa14b1afa0..e4cbb0b410 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.Remoting.Messaging; @@ -22,34 +23,22 @@ namespace Umbraco.Core.Services /// /// Represents the File Service, which is an easy access to operations involving objects like Scripts, Stylesheets and Templates /// - public class FileService : IFileService + public class FileService : RepositoryService, IFileService { - private readonly RepositoryFactory _repositoryFactory; private readonly IUnitOfWorkProvider _fileUowProvider; - private readonly IDatabaseUnitOfWorkProvider _dataUowProvider; private const string PartialViewHeader = "@inherits Umbraco.Web.Mvc.UmbracoTemplatePage"; private const string PartialViewMacroHeader = "@inherits Umbraco.Web.Macros.PartialViewMacroPage"; - [Obsolete("Use the constructors that specify all dependencies instead")] - public FileService() - : this(new RepositoryFactory()) - { } - - [Obsolete("Use the constructors that specify all dependencies instead")] - public FileService(RepositoryFactory repositoryFactory) - : this(new FileUnitOfWorkProvider(), new PetaPocoUnitOfWorkProvider(), repositoryFactory) + public FileService( + IUnitOfWorkProvider fileProvider, + IDatabaseUnitOfWorkProvider dataProvider, + RepositoryFactory repositoryFactory, + ILogger logger, + IEventMessagesFactory eventMessagesFactory) + : base(dataProvider, repositoryFactory, logger, eventMessagesFactory) { - } - - public FileService(IUnitOfWorkProvider fileProvider, IDatabaseUnitOfWorkProvider dataProvider, RepositoryFactory repositoryFactory) - { - if (fileProvider == null) throw new ArgumentNullException("fileProvider"); - if (dataProvider == null) throw new ArgumentNullException("dataProvider"); - if (repositoryFactory == null) throw new ArgumentNullException("repositoryFactory"); - _repositoryFactory = repositoryFactory; - _fileUowProvider = fileProvider; - _dataUowProvider = dataProvider; + _fileUowProvider = fileProvider; } @@ -61,7 +50,7 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable GetStylesheets(params string[] names) { - using (var repository = _repositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), UowProvider.GetUnitOfWork())) { return repository.GetAll(names); } @@ -74,7 +63,7 @@ namespace Umbraco.Core.Services /// A object public Stylesheet GetStylesheetByName(string name) { - using (var repository = _repositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(_fileUowProvider.GetUnitOfWork(), UowProvider.GetUnitOfWork())) { return repository.Get(name); } @@ -91,7 +80,7 @@ namespace Umbraco.Core.Services return; var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateStylesheetRepository(uow, _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) { repository.AddOrUpdate(stylesheet); uow.Commit(); @@ -110,7 +99,7 @@ namespace Umbraco.Core.Services public void DeleteStylesheet(string path, int userId = 0) { var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateStylesheetRepository(uow, _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) { var stylesheet = repository.Get(path); if (stylesheet == null) return; @@ -136,7 +125,7 @@ namespace Umbraco.Core.Services { var uow = _fileUowProvider.GetUnitOfWork(); - using (var repository = _repositoryFactory.CreateStylesheetRepository(uow, _dataUowProvider.GetUnitOfWork())) + using (var repository = RepositoryFactory.CreateStylesheetRepository(uow, UowProvider.GetUnitOfWork())) { return repository.ValidateStylesheet(stylesheet); } @@ -151,7 +140,7 @@ namespace Umbraco.Core.Services /// An enumerable list of objects public IEnumerable - + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/_readme.md b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/_readme.md new file mode 100644 index 0000000000..8feb0377db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/_readme.md @@ -0,0 +1,3 @@ +#Obsolete directives + +Folder contains directives we plan to remove in the next major version of umbraco (8.0) these are not recommended to use. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/autoscale.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/autoscale.directive.js new file mode 100644 index 0000000000..f9ca4350e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/autoscale.directive.js @@ -0,0 +1,40 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:autoScale +* @element div +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @description +* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set +* So if you only want to scale the div to 70 pixels from the bottom you pass "70" + +* @example +* +* +*
+*
+*
+**/ + +angular.module("umbraco.directives") + .directive('autoScale', function ($window) { + return function (scope, el, attrs) { + + var totalOffset = 0; + var offsety = parseInt(attrs.autoScale, 10); + var window = angular.element($window); + if (offsety !== undefined){ + totalOffset += offsety; + } + + setTimeout(function () { + el.height(window.height() - (el.offset().top + totalOffset)); + }, 500); + + window.bind("resize", function () { + el.height(window.height() - (el.offset().top + totalOffset)); + }); + + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/detectfold.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/detectfold.directive.js similarity index 93% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/detectfold.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/detectfold.directive.js index f890ee4989..ac2b86582e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/detectfold.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/detectfold.directive.js @@ -1,9 +1,12 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbPanel +* @name umbraco.directives.directive:detectFold +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @description This is used for the editor buttons to ensure they are displayed correctly if the horizontal overflow of the editor - * exceeds the height of the window +* exceeds the height of the window **/ + angular.module("umbraco.directives.html") .directive('detectFold', function ($timeout, $log, windowResizeListener) { return { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbItemSorter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbItemSorter.directive.js similarity index 88% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbItemSorter.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbItemSorter.directive.js index 8c6d47628c..056bb9559c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbItemSorter.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbItemSorter.directive.js @@ -1,11 +1,14 @@ -/** +/** * @ngdoc directive * @name umbraco.directives.directive:umbItemSorter +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @function * @element ANY * @restrict E * @description A re-usable directive for sorting items **/ + function umbItemSorter(angularHelper) { return { scope: { @@ -13,7 +16,7 @@ function umbItemSorter(angularHelper) { }, restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-item-sorter.html', + templateUrl: 'views/directives/_obsolete/umb-item-sorter.html', link: function(scope, element, attrs, ctrl) { var defaultModel = { okButton: "Ok", @@ -38,7 +41,7 @@ function umbItemSorter(angularHelper) { scope.handleOk = function() { scope.$emit("umbItemSorter.ok"); }; - + //defines the options for the jquery sortable scope.sortableOptions = { axis: 'y', diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbcontentname.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbcontentname.directive.js similarity index 82% rename from src/Umbraco.Web.UI.Client/src/common/directives/editors/umbcontentname.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbcontentname.directive.js index 16b0ab9d23..ec1d260d10 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbcontentname.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbcontentname.directive.js @@ -1,18 +1,22 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbContentName +* @name umbraco.directives.directive:umbContentName +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @restrict E * @function -* @description +* @description * Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. **/ + angular.module("umbraco.directives") .directive('umbContentName', function ($timeout, localizationService) { return { require: "ngModel", restrict: 'E', replace: true, - templateUrl: 'views/directives/umb-content-name.html', + templateUrl: 'views/directives/_obsolete/umb-content-name.html', + scope: { placeholder: '@placeholder', model: '=ngModel', @@ -24,10 +28,10 @@ angular.module("umbraco.directives") if(scope.placeholder && scope.placeholder[0] === "@"){ localizationService.localize(scope.placeholder.substring(1)) .then(function(value){ - scope.placeholder = value; + scope.placeholder = value; }); } - + var mX, mY, distance; function calculateDistance(elem, mouseX, mouseY) { @@ -64,18 +68,18 @@ angular.module("umbraco.directives") scope.goEdit(); } }, 100, false); - + scope.goEdit = function(){ scope.editMode = true; - $timeout(function () { - inputElement.focus(); + $timeout(function () { + inputElement.focus(); }, 100, false); }; scope.exitEdit = function(){ if(scope.model && scope.model !== ""){ - scope.editMode = false; + scope.editMode = false; } }; @@ -85,4 +89,4 @@ angular.module("umbraco.directives") }); } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbheader.directive.js similarity index 88% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbheader.directive.js index 2dedf46586..ccf37b14a2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbheader.directive.js @@ -1,25 +1,28 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbHeader +* @name umbraco.directives.directive:umbHeader +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @restrict E * @function -* @description +* @description * The header on an editor that contains tabs using bootstrap tabs - THIS IS OBSOLETE, use umbTabHeader instead **/ + angular.module("umbraco.directives") .directive('umbHeader', function ($parse, $timeout) { return { restrict: 'E', replace: true, transclude: 'true', - templateUrl: 'views/directives/umb-header.html', + templateUrl: 'views/directives/_obsolete/umb-header.html', //create a new isolated scope assigning a tabs property from the attribute 'tabs' //which is bound to the parent scope property passed in scope: { tabs: "=" }, link: function (scope, iElement, iAttrs) { - + scope.showTabs = iAttrs.tabs ? true : false; scope.visibleTabs = []; @@ -31,11 +34,11 @@ angular.module("umbraco.directives") var tab = {id: val.id, label: val.label}; scope.visibleTabs.push(tab); }); - + //don't process if we cannot or have already done so if (!newValue) {return;} if (!newValue.length || newValue.length === 0){return;} - + //we need to do a timeout here so that the current sync operation can complete // and update the UI, then this will fire and the UI elements will be available. $timeout(function () { @@ -54,8 +57,8 @@ angular.module("umbraco.directives") //stop watching now tabWatch(); }, 200); - + }); } }; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umblogin.directive.js similarity index 53% rename from src/Umbraco.Web.UI.Client/src/common/directives/umblogin.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umblogin.directive.js index 176ccd6c04..a4dbd5fd13 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umblogin.directive.js @@ -1,15 +1,18 @@ -/** +/** * @ngdoc directive -* @name umbraco.directives.directive:login +* @name umbraco.directives.directive:umbLogin +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @function * @element ANY * @restrict E **/ + function loginDirective() { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-login.html' + templateUrl: 'views/directives/_obsolete/umb-login.html' }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umboptionsmenu.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umboptionsmenu.directive.js similarity index 80% rename from src/Umbraco.Web.UI.Client/src/common/directives/umboptionsmenu.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umboptionsmenu.directive.js index 618f19b50e..d51fe73448 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umboptionsmenu.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umboptionsmenu.directive.js @@ -1,3 +1,13 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbOptionsMenu +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* @function +* @element ANY +* @restrict E +**/ + angular.module("umbraco.directives") .directive('umbOptionsMenu', function ($injector, treeService, navigationService, umbModelMapper, appState) { return { @@ -7,7 +17,7 @@ angular.module("umbraco.directives") }, restrict: 'E', replace: true, - templateUrl: 'views/directives/umb-optionsmenu.html', + templateUrl: 'views/directives/_obsolete/umb-optionsmenu.html', link: function (scope, element, attrs, ctrl) { //adds a handler to the context menu item click, we need to handle this differently @@ -22,7 +32,7 @@ angular.module("umbraco.directives") if (!scope.currentNode) { return; } - + //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) appState.setMenuState("currentNode", scope.currentNode); @@ -36,4 +46,4 @@ angular.module("umbraco.directives") } }; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbphotofolder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbphotofolder.directive.js similarity index 91% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbphotofolder.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbphotofolder.directive.js index 84f4f2b974..1c907dae9e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbphotofolder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbphotofolder.directive.js @@ -1,17 +1,20 @@ /** * @ngdoc directive * @name umbraco.directives.directive:umbPhotoFolder +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. * @restrict E **/ + angular.module("umbraco.directives.html") .directive('umbPhotoFolder', function($compile, $log, $timeout, $filter, umbPhotoFolderHelper) { - + return { restrict: 'E', replace: true, require: '?ngModel', terminate: true, - templateUrl: 'views/directives/html/umb-photo-folder.html', + templateUrl: 'views/directives/_obsolete/umb-photo-folder.html', link: function(scope, element, attrs, ngModel) { var lastWatch = null; @@ -24,10 +27,10 @@ angular.module("umbraco.directives.html") scope.clickHandler = scope.$eval(element.attr('on-click')); - + var imagesOnly = element.attr('images-only') === "true"; - - + + var margin = element.attr('border') ? parseInt(element.attr('border'), 10) : 5; var startingIndex = element.attr('baseline') ? parseInt(element.attr('baseline'), 10) : 0; var minWidth = element.attr('min-width') ? parseInt(element.attr('min-width'), 10) : 420; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbsort.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbsort.directive.js similarity index 88% rename from src/Umbraco.Web.UI.Client/src/common/directives/editors/umbsort.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbsort.directive.js index 6166637eed..aab837f916 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbsort.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbsort.directive.js @@ -1,20 +1,24 @@ /** - * @ngdoc directive - * @name umbraco.directives.directive:umbSort - * @element div - * @function - * - * @description - * Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set - * So if you only want to scale the div to 70 pixels from the bottom you pass "70" - * - * @example - - -
-
-
- */ +* @ngdoc directive +* @name umbraco.directives.directive:umbSort +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* +* @element div +* @function +* +* @description +* Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set +* So if you only want to scale the div to 70 pixels from the bottom you pass "70" +* +* @example +* +* +*
+*
+*
+**/ + angular.module("umbraco.directives") .value('umbSortContextInternal',{}) .directive('umbSort', function($log,umbSortContextInternal) { @@ -165,4 +169,4 @@ angular.module("umbraco.directives") } }; - }); \ No newline at end of file + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbtabview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbtabview.directive.js new file mode 100644 index 0000000000..5fe813509e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbtabview.directive.js @@ -0,0 +1,18 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbTabView +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* +* @restrict E +**/ + +angular.module("umbraco.directives") +.directive('umbTabView', function($timeout, $log){ + return { + restrict: 'E', + replace: true, + transclude: 'true', + templateUrl: 'views/directives/_obsolete/umb-tab-view.html' + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbuploaddropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbuploaddropzone.directive.js similarity index 52% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbuploaddropzone.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbuploaddropzone.directive.js index 04e305a7a7..64ca76a6ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbuploaddropzone.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/_obsolete/umbuploaddropzone.directive.js @@ -1,13 +1,17 @@ /** * @ngdoc directive * @name umbraco.directives.directive:umbUploadDropzone +* @deprecated +* We plan to remove this directive in the next major version of umbraco (8.0). The directive is not recommended to use. +* * @restrict E **/ + angular.module("umbraco.directives.html") .directive('umbUploadDropzone', function(){ return { restrict: 'E', replace: true, - templateUrl: 'views/directives/html/umb-upload-dropzone.html' + templateUrl: 'views/directives/_obsolete/umb-upload-dropzone.html' }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/buttongroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/buttongroup.directive.js deleted file mode 100644 index 903970a6ff..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/buttongroup.directive.js +++ /dev/null @@ -1,49 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbProperty -* @restrict E -**/ -angular.module("umbraco.directives") - .directive('buttonGroup', function (contentEditingHelper) { - return { - scope: { - actions: "=", - handler: "=" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/directives/button-group.html', - link: function (scope, element, attrs, ctrl) { - - scope.buttons = []; - - scope.handle = function(action){ - if(scope.handler){ - - } - }; - function processActions() { - var buttons = []; - - angular.forEach(scope.actions, function(action){ - if(angular.isObject(action)){ - buttons.push(action); - }else{ - var btn = contentEditingHelper.getButtonFromAction(action); - if(btn){ - buttons.push(btn); - } - } - }); - - scope.defaultButton = buttons.pop(0); - scope.buttons = buttons; - } - - scope.$watchCollection(scope.actions, function(){ - processActions(); - }); - } - }; - }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/_readme.md b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/_readme.md new file mode 100644 index 0000000000..7ceb11d9a2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/_readme.md @@ -0,0 +1,3 @@ +#Application + +Directives used for the main application window. Navigation, sections etc. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/navresize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/navresize.directive.js similarity index 93% rename from src/Umbraco.Web.UI.Client/src/common/directives/navresize.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/application/navresize.directive.js index b4ee63b8dc..8addd6faf6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/navresize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/navresize.directive.js @@ -2,7 +2,7 @@ * @ngdoc directive * @name umbraco.directives.directive:navResize * @restrict A - * + * * @description * Handles how the navigation responds to window resizing and controls how the draggable resize panel works **/ @@ -31,6 +31,7 @@ angular.module("umbraco.directives") resize: function(e, ui) { var wrapper = $("#mainwrapper"); var contentPanel = $("#contentwrapper"); + var umbNotification = $("#umb-notifications-wrapper"); var apps = $("#applications"); var bottomBar = contentPanel.find(".umb-bottom-bar"); var navOffeset = $("#navOffset"); @@ -39,11 +40,12 @@ angular.module("umbraco.directives") contentPanel.css({ left: leftPanelWidth }); bottomBar.css({ left: leftPanelWidth }); + umbNotification.css({ left: leftPanelWidth }); navOffeset.css({ "margin-left": ui.element.outerWidth() }); }, stop: function (e, ui) { - + } }); @@ -55,11 +57,13 @@ angular.module("umbraco.directives") if (resizeEnabled) { //kill the resize element.resizable("destroy"); - element.css("width", ""); + var navInnerContainer = element.find(".navigation-inner-container"); + navInnerContainer.css("width", ""); $("#contentwrapper").css("left", ""); + $("#umb-notifications-wrapper").css("left", ""); $("#navOffset").css("margin-left", ""); resizeEnabled = false; @@ -92,7 +96,7 @@ angular.module("umbraco.directives") scope.$on('$destroy', function () { windowResizeListener.unregister(resizeCallback); for (var e in evts) { - eventsService.unsubscribe(evts[e]); + eventsService.unsubscribe(evts[e]); } var navInnerContainer = element.find(".navigation-inner-container"); navInnerContainer.resizable("destroy"); @@ -104,4 +108,4 @@ angular.module("umbraco.directives") setTreeMode(); } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/sectionicon.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/sectionicon.directive.js similarity index 91% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/sectionicon.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/application/sectionicon.directive.js index d6a4537262..28b6620aeb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/sectionicon.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/sectionicon.directive.js @@ -17,7 +17,7 @@ if(convert){ element.html(""); }else{ - element.html(""); + element.html(""); } //it's a file, normally legacy so look in the icon tray images } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbcontextmenu.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js similarity index 87% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbcontextmenu.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js index d0ca9201dd..a21c9e4cf1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbcontextmenu.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js @@ -9,9 +9,9 @@ angular.module("umbraco.directives") }, restrict: 'E', replace: true, - templateUrl: 'views/directives/umb-contextmenu.html', + templateUrl: 'views/components/application/umb-contextmenu.html', link: function (scope, element, attrs, ctrl) { - + //adds a handler to the context menu item click, we need to handle this differently //depending on what the menu item is supposed to do. scope.executeMenuItem = function (action) { @@ -19,4 +19,4 @@ angular.module("umbraco.directives") }; } }; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbnavigation.directive.js similarity index 75% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbnavigation.directive.js index dfee6c1daf..baf0f59643 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbnavigation.directive.js @@ -7,8 +7,8 @@ function umbNavigationDirective() { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-navigation.html' + templateUrl: 'views/components/application/umb-navigation.html' }; } -angular.module('umbraco.directives').directive("umbNavigation", umbNavigationDirective); \ No newline at end of file +angular.module('umbraco.directives').directive("umbNavigation", umbNavigationDirective); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js similarity index 62% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index dffa973f77..5ce3eac8a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -7,9 +7,9 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-sections.html', + templateUrl: 'views/components/application/umb-sections.html', link: function (scope, element, attr, ctrl) { - + //setup scope vars scope.maxSections = 7; scope.overflowingSections = 0; @@ -18,7 +18,7 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se scope.showTray = false; //appState.getGlobalState("showTray"); scope.stickyNavigation = appState.getGlobalState("stickyNavigation"); scope.needTray = false; - scope.trayAnimation = function() { + scope.trayAnimation = function() { if (scope.showTray) { return 'slide'; } @@ -85,23 +85,74 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se //on page resize window.onresize = calculateHeight; - + scope.avatarClick = function(){ - navigationService.showUserDialog(); + + if(scope.helpDialog) { + closeHelpDialog(); + } + + if(!scope.userDialog) { + scope.userDialog = { + view: "user", + show: true, + close: function(oldModel) { + closeUserDialog(); + } + }; + } else { + closeUserDialog(); + } + }; + function closeUserDialog() { + scope.userDialog.show = false; + scope.userDialog = null; + } + scope.helpClick = function(){ - navigationService.showHelpDialog(); + + if(scope.userDialog) { + closeUserDialog(); + } + + if(!scope.helpDialog) { + scope.helpDialog = { + view: "help", + show: true, + close: function(oldModel) { + closeHelpDialog(); + } + }; + } else { + closeHelpDialog(); + } + }; - scope.sectionClick = function (section) { - if (navigationService.userDialog) { - navigationService.userDialog.close(); + function closeHelpDialog() { + scope.helpDialog.show = false; + scope.helpDialog = null; + } + + scope.sectionClick = function (event, section) { + + if (event.ctrlKey || + event.shiftKey || + event.metaKey || // apple + (event.button && event.button === 1) // middle click, >IE9 + everyone else + ) { + return; } - if (navigationService.helpDialog) { - navigationService.helpDialog.close(); + + if (scope.userDialog) { + closeUserDialog(); } - + if (scope.helpDialog) { + closeHelpDialog(); + } + navigationService.hideSearch(); navigationService.showTree(section.alias); $location.path("/" + section.alias); @@ -113,11 +164,11 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se scope.trayClick = function () { // close dialogs - if (navigationService.userDialog) { - navigationService.userDialog.close(); + if (scope.userDialog) { + closeUserDialog(); } - if (navigationService.helpDialog) { - navigationService.helpDialog.close(); + if (scope.helpDialog) { + closeHelpDialog(); } if (appState.getGlobalState("showTray") === true) { @@ -126,7 +177,7 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se navigationService.showTray(); } }; - + loadSections(); } 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 new file mode 100644 index 0000000000..47901a533c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -0,0 +1,135 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbButton +@restrict E +@scope + +@description +Use this directive to render an umbraco button. The directive can be used to generate all types of buttons, set type, style, translation, shortcut and much more. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+            vm.buttonState = "init";
+
+            vm.clickButton = clickButton;
+
+            function clickButton() {
+
+                vm.buttonState = "busy";
+
+                myService.clickButton().then(function() {
+                    vm.buttonState = "success";
+                }, function() {
+                    vm.buttonState = "error";
+                });
+
+            }
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+
+    })();
+
+ +@param {callback} action The button action which should be performed when the button is clicked. +@param {string=} href Url/Path to navigato to. +@param {string=} type Set the button type ("button" or "submit"). +@param {string=} buttonStyle Set the style of the button. The directive uses the default bootstrap styles ("primary", "info", "success", "warning", "danger", "inverse", "link"). +@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). +@param {string=} shortcut Set a keyboard shortcut for the button ("ctrl+c"). +@param {string=} label Set the button label. +@param {string=} labelKey Set a localization key to make a multi lingual button ("general_buttonText"). +@param {string=} icon Set a button icon. Can only be used when buttonStyle is "link". +@param {boolean=} disabled Set to true to disable the button. +**/ + +(function() { + 'use strict'; + + function ButtonDirective($timeout) { + + function link(scope, el, attr, ctrl) { + + scope.style = null; + + function activate() { + + if (!scope.state) { + scope.state = "init"; + } + + if (scope.buttonStyle) { + scope.style = "btn-" + scope.buttonStyle; + } + + } + + activate(); + + var unbindStateWatcher = scope.$watch('state', function(newValue, oldValue) { + + if (newValue === 'success' || newValue === 'error') { + $timeout(function() { + scope.state = 'init'; + }, 2000); + } + + }); + + scope.$on('$destroy', function() { + unbindStateWatcher(); + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-button.html', + link: link, + scope: { + action: "&?", + href: "@?", + type: "@", + buttonStyle: "@?", + state: "=?", + shortcut: "@?", + shortcutWhenHidden: "@", + label: "@?", + labelKey: "@?", + icon: "@?", + disabled: "=" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbButton', ButtonDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js new file mode 100644 index 0000000000..292f0cc786 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbuttongroup.directive.js @@ -0,0 +1,115 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbButtonGroup +@restrict E +@scope + +@description +Use this directive to render a button with a dropdown of alternative actions. + +

Markup example

+
+    
+ + + + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.buttonGroup = {
+                defaultButton: {
+                    labelKey: "general_defaultButton",
+                    hotKey: "ctrl+d",
+                    hotKeyWhenHidden: true,
+                    handler: function() {
+                        // do magic here
+                    }
+                },
+                subButtons: [
+                    {
+                        labelKey: "general_subButton",
+                        hotKey: "ctrl+b",
+                        hotKeyWhenHidden: true,
+                        handler: function() {
+                            // do magic here
+                        }
+                    }
+                ]
+            };
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+
+    })();
+
+ +

Button model description

+
    +
  • + labekKey + (string) - + Set a localization key to make a multi lingual button ("general_buttonText"). +
  • +
  • + hotKey + (array) - + Set a keyboard shortcut for the button ("ctrl+c"). +
  • +
  • + hotKeyWhenHidden + (boolean) - + As a default the hotkeys only works on elements visible in the UI. Set to true to set a hotkey on the hidden sub buttons. +
  • +
  • + handler + (callback) - + Set a callback to handle button click events. +
  • +
+ +@param {object} defaultButton The model of the default button. +@param {array} subButtons Array of sub buttons. +@param {string=} state Set a progress state on the button ("init", "busy", "success", "error"). +@param {string=} direction Set the direction of the dropdown ("up", "down"). +@param {string=} float Set the float of the dropdown. ("left", "right"). +**/ + +(function() { + 'use strict'; + + function ButtonGroupDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/buttons/umb-button-group.html', + scope: { + defaultButton: "=", + subButtons: "=", + state: "=?", + direction: "@?", + float: "@?" + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbButtonGroup', ButtonGroupDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/_readme.md b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/_readme.md new file mode 100644 index 0000000000..0955f9ca71 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/_readme.md @@ -0,0 +1,3 @@ +#Editor + +Directives only used to construct the main editor window. Includes header, footer, menu and navigation. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js new file mode 100644 index 0000000000..241bcc26ea --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheader.directive.js @@ -0,0 +1,58 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeader +@restrict E + +@description +Use this directive to construct a sub header in the main editor window. +The sub header is sticky and will follow along down the page when scrolling. + +

Markup example

+
+    
+ +
+ + + + + + + // sub header content here + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeader', EditorSubHeaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadercontentleft.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadercontentleft.directive.js new file mode 100644 index 0000000000..97d2605d6a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadercontentleft.directive.js @@ -0,0 +1,65 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderContentLeft +@restrict E + +@description +Use this directive to left align content in a sub header in the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + + // left content here + + + + // right content here + + + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderContentLeftDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-left.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeaderContentLeft', EditorSubHeaderContentLeftDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadercontentright.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadercontentright.directive.js new file mode 100644 index 0000000000..ce47db5d96 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadercontentright.directive.js @@ -0,0 +1,65 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderContentRight +@restrict E + +@description +Use this directive to rigt align content in a sub header in the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + + // left content here + + + + // right content here + + + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderSection umbEditorSubHeaderSection}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderContentRightDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-content-right.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeaderContentRight', EditorSubHeaderContentRightDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadersection.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadersection.directive.js new file mode 100644 index 0000000000..7243abeea2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/subheader/umbeditorsubheadersection.directive.js @@ -0,0 +1,73 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorSubHeaderSection +@restrict E + +@description +Use this directive to create sections, divided by borders, in a sub header in the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + + + + // section content here + + + + // section content here + + + + // section content here + + + + + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorSubHeader umbEditorSubHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentLeft umbEditorSubHeaderContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorSubHeaderContentRight umbEditorSubHeaderContentRight}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorSubHeaderSectionDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/subheader/umb-editor-sub-header-section.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubHeaderSection', EditorSubHeaderSectionDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js new file mode 100644 index 0000000000..f2797425c5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js @@ -0,0 +1,66 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbBreadcrumbs +@restrict E +@scope + +@description +Use this directive to generate a list of breadcrumbs. + +

Markup example

+
+    
+ + +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+            vm.ancestors = [];
+
+            myService.getAncestors().then(function(ancestors){
+                vm.ancestors = ancestors;
+            });
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {array} ancestors Array of ancestors +@param {string} entityType The content entity type (member, media, content). +**/ + +(function() { + 'use strict'; + + function BreadcrumbsDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-breadcrumbs.html', + scope: { + ancestors: "=", + entityType: "@" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontainer.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontainer.directive.js new file mode 100644 index 0000000000..dd0b8baf71 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontainer.directive.js @@ -0,0 +1,70 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorContainer +@restrict E + +@description +Use this directive to construct a main content area inside the main editor window. + +

Markup example

+
+    
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorContainerDirective(overlayHelper) { + + function link(scope, el, attr, ctrl) { + + scope.numberOfOverlays = 0; + + scope.$watch(function(){ + return overlayHelper.getNumberOfOverlays(); + }, function (newValue) { + scope.numberOfOverlays = newValue; + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-container.html', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorContainer', EditorContainerDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfooter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfooter.directive.js new file mode 100644 index 0000000000..b642f0724a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfooter.directive.js @@ -0,0 +1,63 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooter +@restrict E + +@description +Use this directive to construct a footer inside the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorFooterDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorFooter', EditorFooterDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfootercontentleft.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfootercontentleft.directive.js new file mode 100644 index 0000000000..630146184c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfootercontentleft.directive.js @@ -0,0 +1,63 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooterContentLeft +@restrict E + +@description +Use this directive to align content left inside the main editor footer. + +

Markup example

+
+    
+ +
+ + + + + + + // align content left + + + + // align content right + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentRight umbEditorFooterContentRight}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorFooterContentLeftDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer-content-left.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorFooterContentLeft', EditorFooterContentLeftDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfootercontentright.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfootercontentright.directive.js new file mode 100644 index 0000000000..e966a871e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorfootercontentright.directive.js @@ -0,0 +1,63 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorFooterContentRight +@restrict E + +@description +Use this directive to align content right inside the main editor footer. + +

Markup example

+
+    
+ +
+ + + + + + + // align content left + + + + // align content right + + + + + + +
+ +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
  • {@link umbraco.directives.directive:umbEditorFooterContentLeft umbEditorFooterContentLeft}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorFooterContentRightDirective() { + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-footer-content-right.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorFooterContentRight', EditorFooterContentRightDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js new file mode 100644 index 0000000000..cc57eb6e74 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -0,0 +1,267 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorHeader +@restrict E +@scope + +@description +Use this directive to construct a header inside the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+ +
+
+ +

Markup example - with tabs

+
+    
+ +
+ + + + + + + + + + +
+ // tab 1 content +
+ +
+ // tab 2 content +
+ +
+
+
+ + + // footer content here + + +
+ +
+ +
+
+ +

Controller example - with tabs

+
+    (function () {
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.content = {
+                name: "",
+                tabs: [
+                    {
+                        id: 1,
+                        label: "Tab 1",
+                        alias: "tab1",
+                        active: true
+                    },
+                    {
+                        id: 2,
+                        label: "Tab 2",
+                        alias: "tab2",
+                        active: false
+                    }
+                ]
+            };
+
+        }
+
+        angular.module("umbraco").controller("MySection.Controller", Controller);
+    })();
+
+ +

Markup example - with sub views

+
+    
+ +
+ + + + + + + + + + + + + + + // footer content here + + + + +
+ +
+
+ +

Controller example - with sub views

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+            vm.content = {
+                name: "",
+                navigation: [
+                    {
+                        "name": "Section 1",
+                        "icon": "icon-document-dashed-line",
+                        "view": "/App_Plugins/path/to/html.html",
+                        "active": true
+                    },
+                    {
+                        "name": "Section 2",
+                        "icon": "icon-list",
+                        "view": "/App_Plugins/path/to/html.html",
+                    }
+                ]
+            };
+
+        }
+
+        angular.module("umbraco").controller("MySection.Controller", Controller);
+    })();
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorView umbEditorView}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
+ +@param {string} name The content name. +@param {array=} tabs Array of tabs. See example above. +@param {array=} navigation Array of sub views. See example above. +@param {boolean=} nameLocked Set to true to lock the name. +@param {object=} menu Add a context menu to the editor. +@param {string=} icon Show and edit the content icon. Opens an overlay to change the icon. +@param {boolean=} hideIcon Set to true to hide icon. +@param {string=} alias show and edit the content alias. +@param {boolean=} hideAlias Set to true to hide alias. +@param {string=} description Add a description to the content. +@param {boolean=} hideDescription Set to true to hide description. + +**/ + +(function() { + 'use strict'; + + function EditorHeaderDirective(iconHelper) { + + function link(scope, el, attr, ctrl) { + + scope.openIconPicker = function() { + scope.dialogModel = { + view: "iconpicker", + show: true, + submit: function (model) { + + /* ensure an icon is selected, because on focus on close button + or an element in background no icon is submitted. So don't clear/update existing icon/preview. + */ + if (model.icon) { + + if (model.color) { + scope.icon = model.icon + " " + model.color; + } else { + scope.icon = model.icon; + } + + // set form to dirty + ctrl.$setDirty(); + } + + scope.dialogModel.show = false; + scope.dialogModel = null; + } + }; + }; + } + + var directive = { + require: '^form', + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-header.html', + scope: { + tabs: "=", + actions: "=", + name: "=", + nameLocked: "=", + menu: "=", + icon: "=", + hideIcon: "@", + alias: "=", + hideAlias: "@", + description: "=", + hideDescription: "@", + navigation: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorHeader', EditorHeaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js new file mode 100644 index 0000000000..b6af7a4ca6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js @@ -0,0 +1,50 @@ +(function() { + 'use strict'; + + function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) { + + function link(scope, el, attr, ctrl) { + + //adds a handler to the context menu item click, we need to handle this differently + //depending on what the menu item is supposed to do. + scope.executeMenuItem = function (action) { + navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); + }; + + //callback method to go and get the options async + scope.getOptions = function () { + + if (!scope.currentNode) { + return; + } + + //when the options item is selected, we need to set the current menu item in appState (since this is synonymous with a menu) + appState.setMenuState("currentNode", scope.currentNode); + + if (!scope.actions) { + treeService.getMenu({ treeNode: scope.currentNode }) + .then(function (data) { + scope.actions = data.menuItems; + }); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-menu.html', + link: link, + scope: { + currentNode: "=", + currentSection: "@" + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorMenu', EditorMenuDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js new file mode 100644 index 0000000000..cceb3ac86f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js @@ -0,0 +1,64 @@ +(function() { + 'use strict'; + + function EditorNavigationDirective() { + + function link(scope, el, attr, ctrl) { + + scope.showNavigation = true; + + scope.clickNavigationItem = function(selectedItem) { + setItemToActive(selectedItem); + runItemAction(selectedItem); + }; + + function runItemAction(selectedItem) { + if (selectedItem.action) { + selectedItem.action(selectedItem); + } + } + + function setItemToActive(selectedItem) { + // set all other views to inactive + if (selectedItem.view) { + + for (var index = 0; index < scope.navigation.length; index++) { + var item = scope.navigation[index]; + item.active = false; + } + + // set view to active + selectedItem.active = true; + + } + } + + function activate() { + + // hide navigation if there is only 1 item + if (scope.navigation.length <= 1) { + scope.showNavigation = false; + } + + } + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-navigation.html', + scope: { + navigation: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives.html').directive('umbEditorNavigation', EditorNavigationDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubviews.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubviews.directive.js new file mode 100644 index 0000000000..db0dad8de7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorsubviews.directive.js @@ -0,0 +1,48 @@ +(function() { + 'use strict'; + + function EditorSubViewsDirective() { + + function link(scope, el, attr, ctrl) { + + scope.activeView = {}; + + // set toolbar from selected navigation item + function setActiveView(items) { + + for (var index = 0; index < items.length; index++) { + + var item = items[index]; + + if (item.active && item.view) { + scope.activeView = item; + } + } + } + + // watch for navigation changes + scope.$watch('subViews', function(newValue, oldValue) { + if (newValue) { + setActiveView(newValue); + } + }, true); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-sub-views.html', + scope: { + subViews: "=", + model: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorSubViews', EditorSubViewsDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorview.directive.js new file mode 100644 index 0000000000..34cb55a794 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorview.directive.js @@ -0,0 +1,90 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEditorView +@restrict E +@scope + +@description +Use this directive to construct the main editor window. + +

Markup example

+
+    
+ +
+ + + + + + + + // main content here + + + + // footer content here + + + + +
+ +
+
+

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+        }
+
+        angular.module("umbraco").controller("MySection.Controller", Controller);
+    })();
+
+ + +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbEditorHeader umbEditorHeader}
  • +
  • {@link umbraco.directives.directive:umbEditorContainer umbEditorContainer}
  • +
  • {@link umbraco.directives.directive:umbEditorFooter umbEditorFooter}
  • +
+**/ + +(function() { + 'use strict'; + + function EditorViewDirective() { + + function link(scope, el, attr) { + + if(attr.footer) { + scope.footer = attr.footer; + } + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-view.html', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEditorView', EditorViewDirective); + +})(); 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 new file mode 100644 index 0000000000..1b9dc090bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -0,0 +1,275 @@ +/** +* @description Utillity directives for key and field events +**/ +angular.module('umbraco.directives') + +.directive('onKeyup', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onKeyup); + }; + elm.on("keyup", f); + scope.$on("$destroy", function(){ elm.off("keyup", f);} ); + } + }; +}) + +.directive('onKeydown', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onKeydown); + }; + elm.on("keydown", f); + scope.$on("$destroy", function(){ elm.off("keydown", f);} ); + } + }; +}) + +.directive('onBlur', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onBlur); + }; + elm.on("blur", f); + scope.$on("$destroy", function(){ elm.off("blur", f);} ); + } + }; +}) + +.directive('onFocus', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onFocus); + }; + elm.on("focus", f); + scope.$on("$destroy", function(){ elm.off("focus", f);} ); + } + }; +}) + +.directive('onDragEnter', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnter); + }; + elm.on("dragenter", f); + scope.$on("$destroy", function(){ elm.off("dragenter", f);} ); + } + }; +}) + +.directive('onDragLeave', function () { + return function (scope, elm, attrs) { + var f = function (event) { + var rect = this.getBoundingClientRect(); + var getXY = function getCursorPosition(event) { + var x, y; + + if (typeof event.clientX === 'undefined') { + // try touch screen + x = event.pageX + document.documentElement.scrollLeft; + y = event.pageY + document.documentElement.scrollTop; + } else { + x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + } + + return { x: x, y : y }; + }; + + var e = getXY(event.originalEvent); + + // Check the mouseEvent coordinates are outside of the rectangle + if (e.x > rect.left + rect.width - 1 || e.x < rect.left || e.y > rect.top + rect.height - 1 || e.y < rect.top) { + scope.$apply(attrs.onDragLeave); + } + }; + + elm.on("dragleave", f); + scope.$on("$destroy", function(){ elm.off("dragleave", f);} ); + }; +}) + +.directive('onDragOver', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragOver); + }; + elm.on("dragover", f); + scope.$on("$destroy", function(){ elm.off("dragover", f);} ); + } + }; +}) + +.directive('onDragStart', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragStart); + }; + elm.on("dragstart", f); + scope.$on("$destroy", function(){ elm.off("dragstart", f);} ); + } + }; +}) + +.directive('onDragEnd', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDragEnd); + }; + elm.on("dragend", f); + scope.$on("$destroy", function(){ elm.off("dragend", f);} ); + } + }; +}) + +.directive('onDrop', function () { + return { + link: function (scope, elm, attrs) { + var f = function () { + scope.$apply(attrs.onDrop); + }; + elm.on("drop", f); + scope.$on("$destroy", function(){ elm.off("drop", f);} ); + } + }; +}) + +.directive('onOutsideClick', function ($timeout) { + return function (scope, element, attrs) { + + var eventBindings = []; + + function oneTimeClick(event) { + var el = event.target.nodeName; + + //ignore link and button clicks + var els = ["INPUT","A","BUTTON"]; + if(els.indexOf(el) >= 0){return;} + + // ignore children of links and buttons + // ignore clicks on new overlay + var parents = $(event.target).parents("a,button,.umb-overlay"); + if(parents.length > 0){ + return; + } + + // ignore clicks on dialog from old dialog service + var oldDialog = $(el).parents("#old-dialog-service"); + if (oldDialog.length === 1) { + return; + } + + // ignore clicks in tinyMCE dropdown(floatpanel) + var floatpanel = $(el).parents(".mce-floatpanel"); + if (floatpanel.length === 1) { + return; + } + + //ignore clicks inside this element + if( $(element).has( $(event.target) ).length > 0 ){ + return; + } + + scope.$apply(attrs.onOutsideClick); + } + + + $timeout(function(){ + + if ("bindClickOn" in attrs) { + + eventBindings.push(scope.$watch(function() { + return attrs.bindClickOn; + }, function(newValue) { + if (newValue === "true") { + $(document).on("click", oneTimeClick); + } else { + $(document).off("click", oneTimeClick); + } + })); + + } else { + $(document).on("click", oneTimeClick); + } + + scope.$on("$destroy", function() { + $(document).off("click", oneTimeClick); + + // unbind watchers + for (var e in eventBindings) { + eventBindings[e](); + } + + }); + }); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution. + + }; +}) + +.directive('onRightClick',function(){ + + document.oncontextmenu = function (e) { + if(e.target.hasAttribute('on-right-click')) { + e.preventDefault(); + e.stopPropagation(); + return false; + } + }; + + return function(scope,el,attrs){ + el.on('contextmenu',function(e){ + e.preventDefault(); + e.stopPropagation(); + scope.$apply(attrs.onRightClick); + return false; + }); + }; +}) + +.directive('onDelayedMouseleave', function ($timeout, $parse) { + return { + + restrict: 'A', + + link: function (scope, element, attrs, ctrl) { + var active = false; + var fn = $parse(attrs.onDelayedMouseleave); + + var leave_f = function(event) { + var callback = function() { + fn(scope, {$event:event}); + }; + + active = false; + $timeout(function(){ + if(active === false){ + scope.$apply(callback); + } + }, 650); + }; + + var enter_f = function(event, args){ + active = true; + }; + + + element.on("mouseleave", leave_f); + element.on("mouseenter", enter_f); + + //unsub events + scope.$on("$destroy", function(){ + element.off("mouseleave", leave_f); + element.off("mouseenter", enter_f); + }); + } + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/_readme.md b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/_readme.md new file mode 100644 index 0000000000..e82cf0c131 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/_readme.md @@ -0,0 +1,3 @@ +#Forms + +Directives used to enhance form elements. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/checklistmodel.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/checklistmodel.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/checklistmodel.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/contenteditable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/contenteditable.directive.js new file mode 100644 index 0000000000..08eae2f378 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/contenteditable.directive.js @@ -0,0 +1,35 @@ +angular.module("umbraco.directives") +.directive("contenteditable", function() { + + return { + require: "ngModel", + link: function(scope, element, attrs, ngModel) { + + function read() { + ngModel.$setViewValue(element.html()); + } + + ngModel.$render = function() { + element.html(ngModel.$viewValue || ""); + }; + + + element.bind("focus", function(){ + + var range = document.createRange(); + range.selectNodeContents(element[0]); + + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + + }); + + element.bind("blur keyup change", function() { + scope.$apply(read); + }); + } + + }; + +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/fixnumber.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/fixnumber.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/fixnumber.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/fixnumber.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/focuswhen.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js similarity index 76% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/focuswhen.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js index 7422dcfc35..5d57b29175 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/focuswhen.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js @@ -4,7 +4,9 @@ angular.module("umbraco.directives").directive('focusWhen', function ($timeout) link: function (scope, elm, attrs, ctrl) { attrs.$observe("focusWhen", function (newValue) { if (newValue === "true") { - elm.focus(); + $timeout(function () { + elm.focus(); + }); } }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/hexbackgroundcolor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hexbackgroundcolor.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/hexbackgroundcolor.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hexbackgroundcolor.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hotkey.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hotkey.directive.js new file mode 100644 index 0000000000..240f666632 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/hotkey.directive.js @@ -0,0 +1,70 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:hotkey +**/ + +angular.module("umbraco.directives") + .directive('hotkey', function($window, keyboardService, $log) { + + return function(scope, el, attrs) { + + var options = {}; + var keyCombo = attrs.hotkey; + + if (!keyCombo) { + //support data binding + keyCombo = scope.$eval(attrs["hotkey"]); + } + + function activate() { + + if (keyCombo) { + + // disable shortcuts in input fields if keycombo is 1 character + if (keyCombo.length === 1) { + options = { + inputDisabled: true + }; + } + + keyboardService.bind(keyCombo, function() { + + var element = $(el); + var activeElementType = document.activeElement.tagName; + var clickableElements = ["A", "BUTTON"]; + + if (element.is("a,div,button,input[type='button'],input[type='submit'],input[type='checkbox']") && !element.is(':disabled')) { + + if (element.is(':visible') || attrs.hotkeyWhenHidden) { + + if (attrs.hotkeyWhen && attrs.hotkeyWhen === "false") { + return; + } + + // when keycombo is enter and a link or button has focus - click the link or button instead of using the hotkey + if (keyCombo === "enter" && clickableElements.indexOf(activeElementType) === 0) { + document.activeElement.click(); + } else { + element.click(); + } + + } + + } else { + element.focus(); + } + + }, options); + + el.on('$destroy', function() { + keyboardService.unbind(keyCombo); + }); + + } + + } + + activate(); + + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/preventdefault.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/preventdefault.directive.js similarity index 66% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/preventdefault.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/preventdefault.directive.js index d1e279a9ac..df665be727 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/preventdefault.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/preventdefault.directive.js @@ -1,13 +1,23 @@ /** -* @ngdoc directive -* @name umbraco.directives.directive:preventDefault +@ngdoc directive +@name umbraco.directives.directive:preventDefault + +@description +Use this directive to prevent default action of an element. Effectively implementing jQuery's preventdefault + +

Markup example

+ +
+    Don't go to Umbraco.com
+
+ **/ angular.module("umbraco.directives") .directive('preventDefault', function() { return function(scope, element, attrs) { var enabled = true; - //check if there's a value for the attribute, if there is and it's false then we conditionally don't + //check if there's a value for the attribute, if there is and it's false then we conditionally don't //prevent default. if (attrs.preventDefault) { attrs.$observe("preventDefault", function (newVal) { @@ -26,4 +36,4 @@ angular.module("umbraco.directives") } }); }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/prevententersubmit.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/prevententersubmit.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/prevententersubmit.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/resizetocontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/resizetocontent.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/resizetocontent.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/resizetocontent.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/selectonfocus.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/selectonfocus.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/util/selectonfocus.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/selectonfocus.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoFocus.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js similarity index 85% rename from src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoFocus.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js index bf415ff25d..bb65e94593 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoFocus.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautofocus.directive.js @@ -4,7 +4,7 @@ angular.module("umbraco.directives") return function(scope, element, attr){ var update = function() { //if it uses its default naming - if(element.val() === ""){ + if(element.val() === "" || attr.focusOnFilled){ element.focus(); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js new file mode 100644 index 0000000000..8a456670c3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js @@ -0,0 +1,157 @@ +angular.module("umbraco.directives") + .directive('umbAutoResize', function($timeout) { + return { + require: ["^?umbTabs", "ngModel"], + link: function(scope, element, attr, controllersArr) { + + var domEl = element[0]; + var domElType = domEl.type; + var umbTabsController = controllersArr[0]; + var ngModelController = controllersArr[1]; + + // IE elements + var isIEFlag = false; + var wrapper = angular.element('#umb-ie-resize-input-wrapper'); + var mirror = angular.element(''); + + function isIE() { + + var ua = window.navigator.userAgent; + var msie = ua.indexOf("MSIE "); + + if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) { + return true; + } else { + return false; + } + + } + + function activate() { + + // check if browser is Internet Explorere + isIEFlag = isIE(); + + // scrollWidth on element does not work in IE on inputs + // we have to do some dirty dom element copying. + if (isIEFlag === true && domElType === "text") { + setupInternetExplorerElements(); + } + + } + + function setupInternetExplorerElements() { + + if (!wrapper.length) { + wrapper = angular.element('
'); + angular.element('body').append(wrapper); + } + + angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', + 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', + 'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'borderLeftStyle', 'borderRightStyle', + 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' + ], function(value) { + mirror.css(value, element.css(value)); + }); + + wrapper.append(mirror); + + } + + function resizeInternetExplorerInput() { + + mirror.text(element.val() || attr.placeholder); + element.css('width', mirror.outerWidth() + 1); + + } + + function resizeInput() { + + if (domEl.scrollWidth !== domEl.clientWidth) { + if (ngModelController.$modelValue) { + element.width(domEl.scrollWidth); + } + } + + if(!ngModelController.$modelValue && attr.placeholder) { + attr.$set('size', attr.placeholder.length); + element.width('auto'); + } + + } + + function resizeTextarea() { + + if(domEl.scrollHeight !== domEl.clientHeight) { + + element.height(domEl.scrollHeight); + + } + + } + + var update = function(force) { + + + if (force === true) { + + if (domElType === "textarea") { + element.height(0); + } else if (domElType === "text") { + element.width(0); + } + + } + + + if (isIEFlag === true && domElType === "text") { + + resizeInternetExplorerInput(); + + } else { + + if (domElType === "textarea") { + + resizeTextarea(); + + } else if (domElType === "text") { + + resizeInput(); + + } + + } + + }; + + activate(); + + //listen for tab changes + if (umbTabsController != null) { + umbTabsController.onTabShown(function(args) { + update(); + }); + } + + // listen for ng-model changes + var unbindModelWatcher = scope.$watch(function() { + return ngModelController.$modelValue; + }, function(newValue) { + update(true); + }); + + scope.$on('$destroy', function() { + element.unbind('keyup keydown keypress change', update); + element.unbind('blur', update(true)); + unbindModelWatcher(); + + // clean up IE dom element + if (isIEFlag === true && domElType === "text") { + mirror.remove(); + } + + }); + } + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbRawModel.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbrawmodel.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/editors/umbRawModel.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbrawmodel.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbselectwhen.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbselectwhen.directive.js new file mode 100644 index 0000000000..b8986c02d3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbselectwhen.directive.js @@ -0,0 +1,28 @@ +(function() { + 'use strict'; + + function SelectWhen($timeout) { + + function link(scope, el, attr, ctrl) { + + attr.$observe("umbSelectWhen", function(newValue) { + if (newValue === "true") { + $timeout(function() { + el.select(); + }); + } + }); + + } + + var directive = { + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbSelectWhen', SelectWhen); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js similarity index 78% rename from src/Umbraco.Web.UI.Client/src/common/directives/grid/grid.rte.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 257901fade..0692a10e90 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -7,7 +7,11 @@ angular.module("umbraco.directives") onClick: '&', onFocus: '&', onBlur: '&', - configuration:"=" + configuration:"=", + onMediaPickerClick: "=", + onEmbedClick: "=", + onMacroPickerClick: "=", + onLinkPickerClick: "=" }, template: "", replace: true, @@ -26,7 +30,7 @@ angular.module("umbraco.directives") //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align]"; + var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style]"; var invalidElements = tinyMceConfig.inValidElements; var plugins = _.map(tinyMceConfig.plugins, function (plugin) { @@ -56,7 +60,7 @@ angular.module("umbraco.directives") if(scope.configuration && scope.configuration.stylesheets){ angular.forEach(scope.configuration.stylesheets, function(stylesheet, key){ - stylesheets.push("/css/" + stylesheet + ".css"); + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + stylesheet + ".css"); await.push(stylesheetResource.getRulesByName(stylesheet).then(function (rules) { angular.forEach(rules, function (rule) { var r = {}; @@ -100,12 +104,38 @@ angular.module("umbraco.directives") statusbar: false, relative_urls: false, toolbar: toolbar, - content_css: stylesheets.join(','), - style_formats: styleFormats + content_css: stylesheets, + style_formats: styleFormats, + autoresize_bottom_margin: 0 }; if (tinyMceConfig.customConfig) { + + //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to + // convert it to json instead of having it as a string since this is what tinymce requires + for (var i in tinyMceConfig.customConfig) { + var val = tinyMceConfig.customConfig[i]; + if (val) { + val = val.toString().trim(); + if (val.detectIsJson()) { + try { + tinyMceConfig.customConfig[i] = JSON.parse(val); + //now we need to check if this custom config key is defined in our baseline, if it is we don't want to + //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise + //if it's an object it will overwrite the baseline + if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in angular.extend + tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); + } + } + catch (e) { + //cannot parse, we'll just leave it + } + } + } + } + angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); } @@ -122,11 +152,6 @@ angular.module("umbraco.directives") editor.getBody().setAttribute('spellcheck', true); - //hide toolbar by default - $(editor.editorContainer) - .find(".mce-toolbar") - .css("visibility", "hidden"); - //force overflow to hidden to prevent no needed scroll editor.getBody().style.overflow = "hidden"; @@ -137,32 +162,6 @@ angular.module("umbraco.directives") }, 400); }); - - // pin toolbar to top of screen if we have focus and it scrolls off the screen - var pinToolbar = function () { - - var _toolbar = $(editor.editorContainer).find(".mce-toolbar"); - var toolbarHeight = _toolbar.height(); - - var _tinyMce = $(editor.editorContainer); - var tinyMceRect = _tinyMce[0].getBoundingClientRect(); - var tinyMceTop = tinyMceRect.top; - var tinyMceBottom = tinyMceRect.bottom; - - if (tinyMceTop < 100 && (tinyMceBottom > (100 + toolbarHeight))) { - _toolbar - .css("visibility", "visible") - .css("position", "fixed") - .css("top", "100px") - .css("margin-top", "0"); - } else { - _toolbar - .css("visibility", "visible") - .css("position", "absolute") - .css("top", "auto") - .css("margin-top", (-toolbarHeight - 2) + "px"); - } - }; //when we leave the editor (maybe) editor.on('blur', function (e) { @@ -177,8 +176,6 @@ angular.module("umbraco.directives") scope.onBlur(); } - _toolbar.css("visibility", "hidden"); - $('.umb-panel-body').off('scroll', pinToolbar); }); }); @@ -190,8 +187,6 @@ angular.module("umbraco.directives") scope.onFocus(); } - pinToolbar(); - $('.umb-panel-body').on('scroll', pinToolbar); }); }); @@ -203,8 +198,6 @@ angular.module("umbraco.directives") scope.onClick(); } - pinToolbar(); - $('.umb-panel-body').on('scroll', pinToolbar); }); }); @@ -241,18 +234,33 @@ angular.module("umbraco.directives") $(e.target).attr("data-mce-src", path + qs); }); + //Create the insert link plugin + tinyMceService.createLinkPicker(editor, scope, function(currentTarget, anchorElement){ + if(scope.onLinkPickerClick) { + scope.onLinkPickerClick(editor, currentTarget, anchorElement); + } + }); //Create the insert media plugin - tinyMceService.createMediaPicker(editor, scope); + tinyMceService.createMediaPicker(editor, scope, function(currentTarget, userData){ + if(scope.onMediaPickerClick) { + scope.onMediaPickerClick(editor, currentTarget, userData); + } + }); //Create the embedded plugin - tinyMceService.createInsertEmbeddedMedia(editor, scope); - - //Create the insert link plugin - //tinyMceService.createLinkPicker(editor, scope); + tinyMceService.createInsertEmbeddedMedia(editor, scope, function(){ + if(scope.onEmbedClick) { + scope.onEmbedClick(editor); + } + }); //Create the insert macro plugin - tinyMceService.createInsertMacro(editor, scope); + tinyMceService.createInsertMacro(editor, scope, function(dialogData){ + if(scope.onMacroPickerClick) { + scope.onMacroPickerClick(editor, dialogData); + } + }); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/readme.md b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/readme.md similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/readme.md rename to src/Umbraco.Web.UI.Client/src/common/directives/components/html/readme.md diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbcontrolgroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js similarity index 86% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbcontrolgroup.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js index f371dba9b5..70baa6718b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbcontrolgroup.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbcontrolgroup.directive.js @@ -1,6 +1,6 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbProperty +* @name umbraco.directives.directive:umbControlGroup * @restrict E **/ angular.module("umbraco.directives.html") @@ -15,8 +15,8 @@ angular.module("umbraco.directives.html") require: '?^form', transclude: true, restrict: 'E', - replace: true, - templateUrl: 'views/directives/html/umb-control-group.html', + replace: true, + templateUrl: 'views/components/html/umb-control-group.html', link: function (scope, element, attr, formCtrl) { scope.formValid = function() { @@ -43,4 +43,4 @@ angular.module("umbraco.directives.html") } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbpane.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbpane.directive.js similarity index 57% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbpane.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbpane.directive.js index 93ce6f13a4..8c01fb4d4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbpane.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbpane.directive.js @@ -1,6 +1,6 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbProperty +* @name umbraco.directives.directive:umbPane * @restrict E **/ angular.module("umbraco.directives.html") @@ -8,7 +8,7 @@ angular.module("umbraco.directives.html") return { transclude: true, restrict: 'E', - replace: true, - templateUrl: 'views/directives/html/umb-pane.html' + replace: true, + templateUrl: 'views/components/html/umb-pane.html' }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbpanel.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbpanel.directive.js similarity index 77% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbpanel.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbpanel.directive.js index 5b32941fff..7c69437f4a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbpanel.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/html/umbpanel.directive.js @@ -9,6 +9,6 @@ angular.module("umbraco.directives.html") restrict: 'E', replace: true, transclude: 'true', - templateUrl: 'views/directives/html/umb-panel.html' + templateUrl: 'views/components/html/umb-panel.html' }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagecrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js similarity index 88% rename from src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagecrop.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js index 74c205417a..135d806aa4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagecrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js @@ -5,12 +5,12 @@ * @function **/ angular.module("umbraco.directives") - .directive('umbImageCrop', + .directive('umbImageCrop', function ($timeout, localizationService, cropperHelper, $log) { return { restrict: 'E', replace: true, - templateUrl: 'views/directives/imaging/umb-image-crop.html', + templateUrl: 'views/components/imaging/umb-image-crop.html', scope: { src: '=', width: '@', @@ -39,15 +39,15 @@ angular.module("umbraco.directives") //live rendering of viewport and image styles scope.style = function () { - return { + return { 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', - 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' + 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' }; }; - + //elements - var $viewport = element.find(".viewport"); + var $viewport = element.find(".viewport"); var $image = element.find("img"); var $overlay = element.find(".overlay"); var $container = element.find(".crop-container"); @@ -64,7 +64,7 @@ angular.module("umbraco.directives") }; - var setDimensions = function(originalImage){ + var setDimensions = function(originalImage){ originalImage.width("auto"); originalImage.height("auto"); @@ -86,15 +86,15 @@ angular.module("umbraco.directives") var _viewPortH = parseInt(scope.height, 10); //if we set a constraint we will scale it down if needed - if(scope.maxSize){ + if(scope.maxSize){ var ratioCalculation = cropperHelper.scaleToMaxSize( - _viewPortW, + _viewPortW, _viewPortH, scope.maxSize); //so if we have a max size, override the thumb sizes _viewPortW = ratioCalculation.width; - _viewPortH = ratioCalculation.height; + _viewPortH = ratioCalculation.height; } scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; @@ -106,12 +106,12 @@ angular.module("umbraco.directives") //when loading an image without any crop info, we center and fit it var resizeImageToEditor = function(){ - //returns size fitting the cropper + //returns size fitting the cropper var size = cropperHelper.calculateAspectRatioFit( - scope.dimensions.image.width, - scope.dimensions.image.height, - scope.dimensions.cropper.width, - scope.dimensions.cropper.height, + scope.dimensions.image.width, + scope.dimensions.image.height, + scope.dimensions.cropper.width, + scope.dimensions.cropper.height, true); //sets the image size and updates the scope @@ -145,16 +145,16 @@ angular.module("umbraco.directives") //resize the image to a predefined crop coordinate var resizeImageToCrop = function(){ scope.dimensions.image = cropperHelper.convertToStyle( - scope.crop, + scope.crop, {width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight}, scope.dimensions.cropper, scope.dimensions.margin); var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.dimensions.image.originalWidth, - scope.dimensions.image.originalHeight, - scope.dimensions.cropper.width, - scope.dimensions.cropper.height, + scope.dimensions.image.originalWidth, + scope.dimensions.image.originalHeight, + scope.dimensions.cropper.width, + scope.dimensions.cropper.height, true); scope.dimensions.scale.current = scope.dimensions.image.ratio; @@ -169,7 +169,7 @@ angular.module("umbraco.directives") var validatePosition = function(left, top){ if(left > constraints.left.max) { - left = constraints.left.max; + left = constraints.left.max; } if(left <= constraints.left.min){ @@ -178,7 +178,7 @@ angular.module("umbraco.directives") if(top > constraints.top.max) { - top = constraints.top.max; + top = constraints.top.max; } if(top <= constraints.top.min){ top = constraints.top.min; @@ -186,17 +186,17 @@ angular.module("umbraco.directives") if(scope.dimensions.image.left !== left){ scope.dimensions.image.left = left; - } - + } + if(scope.dimensions.image.top !== top){ scope.dimensions.image.top = top; } - }; + }; - //sets scope.crop to the recalculated % based crop + //sets scope.crop to the recalculated % based crop var calculateCropBox = function(){ - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); + scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); }; @@ -218,7 +218,7 @@ angular.module("umbraco.directives") }); } }); - + var init = function(image){ @@ -252,10 +252,10 @@ angular.module("umbraco.directives") var throttledResizing = _.throttle(function(){ resizeImageToScale(scope.dimensions.scale.current); - calculateCropBox(); + calculateCropBox(); }, 100); - + //happens when we change the scale scope.$watch("dimensions.scale.current", function(){ if(scope.loaded){ @@ -270,9 +270,9 @@ angular.module("umbraco.directives") scope.$apply(function(){ scope.dimensions.scale.current = ranger.val(); }); - }); + }); } - + //// INIT ///// $image.load(function(){ $timeout(function(){ @@ -281,4 +281,4 @@ angular.module("umbraco.directives") }); } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js similarity index 58% rename from src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagegravity.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index 3de777cee9..2a183564f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -1,23 +1,23 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbCropsy +* @name umbraco.directives.directive:umbImageGravity * @restrict E * @function -* @description -* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. +* @description **/ angular.module("umbraco.directives") .directive('umbImageGravity', function ($timeout, localizationService, $log) { return { restrict: 'E', replace: true, - templateUrl: 'views/directives/imaging/umb-image-gravity.html', + templateUrl: 'views/components/imaging/umb-image-gravity.html', scope: { src: '=', - center: "=" + center: "=", + onImageLoaded: "=" }, link: function(scope, element, attrs) { - + //Internal values for keeping track of the dot and the size of the editor scope.dimensions = { width: 0, @@ -26,11 +26,13 @@ angular.module("umbraco.directives") top: 0 }; + scope.loaded = false; + //elements - var $viewport = element.find(".viewport"); + var $viewport = element.find(".viewport"); var $image = element.find("img"); var $overlay = element.find(".overlay"); - + scope.style = function () { if(scope.dimensions.width <= 0){ setDimensions(); @@ -38,10 +40,23 @@ angular.module("umbraco.directives") return { 'top': scope.dimensions.top + 'px', - 'left': scope.dimensions.left + 'px' + 'left': scope.dimensions.left + 'px' }; }; + scope.setFocalPoint = function(event) { + + scope.$emit("imageFocalPointStart"); + + var offsetX = event.offsetX - 10; + var offsetY = event.offsetY - 10; + + calculateGravity(offsetX, offsetY); + + lazyEndEvent(); + + }; + var setDimensions = function(){ scope.dimensions.width = $image.width(); scope.dimensions.height = $image.height(); @@ -52,22 +67,22 @@ angular.module("umbraco.directives") }else{ scope.center = { left: 0.5, top: 0.5 }; } - }; + }; - var calculateGravity = function(){ - scope.dimensions.left = $overlay[0].offsetLeft; - scope.dimensions.top = $overlay[0].offsetTop; + var calculateGravity = function(offsetX, offsetY){ + scope.dimensions.left = offsetX; + scope.dimensions.top = offsetY; scope.center.left = (scope.dimensions.left+10) / scope.dimensions.width; scope.center.top = (scope.dimensions.top+10) / scope.dimensions.height; }; - + var lazyEndEvent = _.debounce(function(){ scope.$apply(function(){ scope.$emit("imageFocalPointStop"); }); }, 2000); - + //Drag and drop positioning, using jquery ui draggable //TODO ensure that the point doesnt go outside the box @@ -80,7 +95,9 @@ angular.module("umbraco.directives") }, stop: function() { scope.$apply(function(){ - calculateGravity(); + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); }); lazyEndEvent(); @@ -91,8 +108,26 @@ angular.module("umbraco.directives") $image.load(function(){ $timeout(function(){ setDimensions(); + scope.loaded = true; + scope.onImageLoaded(); }); }); + + $(window).on('resize.umbImageGravity', function(){ + scope.$apply(function(){ + $timeout(function(){ + setDimensions(); + }); + var offsetX = $overlay[0].offsetLeft; + var offsetY = $overlay[0].offsetTop; + calculateGravity(offsetX, offsetY); + }); + }); + + scope.$on('$destroy', function() { + $(window).off('.umbImageGravity'); + }); + } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagethumbnail.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js similarity index 77% rename from src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagethumbnail.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js index 803ab278d0..dd6dcffc31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagethumbnail.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagethumbnail.directive.js @@ -1,19 +1,18 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbCropsy +* @name umbraco.directives.directive:umbImageThumbnail * @restrict E * @function -* @description -* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. +* @description **/ angular.module("umbraco.directives") - .directive('umbImageThumbnail', + .directive('umbImageThumbnail', function ($timeout, localizationService, cropperHelper, $log) { return { restrict: 'E', replace: true, - templateUrl: 'views/directives/imaging/umb-image-thumbnail.html', - + templateUrl: 'views/components/imaging/umb-image-thumbnail.html', + scope: { src: '=', width: '@', @@ -22,10 +21,11 @@ angular.module("umbraco.directives") crop: "=", maxSize: '@' }, - + link: function(scope, element, attrs) { //// INIT ///// var $image = element.find("img"); + scope.loaded = false; $image.load(function(){ $timeout(function(){ @@ -40,10 +40,10 @@ angular.module("umbraco.directives") //we do not compare to the image dimensions, but the thumbs if(scope.maxSize){ var ratioCalculation = cropperHelper.calculateAspectRatioFit( - scope.width, + scope.width, scope.height, - scope.maxSize, - scope.maxSize, + scope.maxSize, + scope.maxSize, false); //so if we have a max size, override the thumb sizes @@ -51,7 +51,8 @@ angular.module("umbraco.directives") scope.height = ratioCalculation.height; } - setPreviewStyle(); + setPreviewStyle(); + scope.loaded = true; }); }); @@ -65,22 +66,22 @@ angular.module("umbraco.directives") scope.$watch("center", function(){ setPreviewStyle(); }, true); - + function setPreviewStyle(){ if(scope.crop && scope.image){ scope.preview = cropperHelper.convertToStyle( - scope.crop, + scope.crop, scope.image, {width: scope.width, height: scope.height}, 0); }else if(scope.image){ - //returns size fitting the cropper + //returns size fitting the cropper var p = cropperHelper.calculateAspectRatioFit( - scope.image.width, - scope.image.height, - scope.width, - scope.height, + scope.image.width, + scope.image.height, + scope.width, + scope.height, true); @@ -98,4 +99,4 @@ angular.module("umbraco.directives") } } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js new file mode 100644 index 0000000000..52271961a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js @@ -0,0 +1,43 @@ +angular.module("umbraco.directives") + + .directive('localize', function ($log, localizationService) { + return { + restrict: 'E', + scope:{ + key: '@' + }, + replace: true, + + link: function (scope, element, attrs) { + var key = scope.key; + localizationService.localize(key).then(function(value){ + element.html(value); + }); + } + }; + }) + + .directive('localize', function ($log, localizationService) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + var keys = attrs.localize.split(','); + + angular.forEach(keys, function(value, key){ + var attr = element.attr(value); + + if(attr){ + if(attr[0] === '@'){ + + var t = localizationService.tokenize(attr.substring(1), scope); + localizationService.localize(t.key, t.tokens).then(function(val){ + element.attr(value, val); + }); + + } + } + }); + } + }; + + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/notifications/umbnotifications.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/notifications/umbnotifications.directive.js new file mode 100644 index 0000000000..d9f8f76576 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/notifications/umbnotifications.directive.js @@ -0,0 +1,36 @@ +/** + * @ngdoc directive + * @name umbraco.directives.directive:umbNotifications + */ + +(function() { + 'use strict'; + + function NotificationDirective(notificationsService) { + + function link(scope, el, attr, ctrl) { + + //subscribes to notifications in the notification service + scope.notifications = notificationsService.current; + scope.$watch('notificationsService.current', function (newVal, oldVal, scope) { + if (newVal) { + scope.notifications = newVal; + } + }); + + } + + var directive = { + restrict: "E", + replace: true, + templateUrl: 'views/components/notifications/umb-notifications.html', + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbNotifications', NotificationDirective); + +})(); 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 new file mode 100644 index 0000000000..d69c7f6ba0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -0,0 +1,704 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbOverlay +@restrict E +@scope + +@description + +

Markup example

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

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.openOverlay = openOverlay;
+
+            function openOverlay() {
+
+                vm.overlay = {
+                    view: "mediapicker",
+                    show: true,
+                    submit: function(model) {
+
+                        vm.overlay.show = false;
+                        vm.overlay = null;
+                    },
+                    close: function(oldModel) {
+                        vm.overlay.show = false;
+                        vm.overlay = null;
+                    }
+                }
+
+            };
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +

General Options

+Lorem ipsum dolor sit amet.. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
model.titleStringSet the title of the overlay.
model.subTitleStringSet the subtitle of the overlay.
model.submitButtonLabelStringSet an alternate submit button text
model.submitButtonLabelKeyStringSet an alternate submit button label key for localized texts
model.hideSubmitButtonBooleanHides the submit button
model.closeButtonLabelStringSet an alternate close button text
model.closeButtonLabelKeyStringSet an alternate close button label key for localized texts
model.showBooleanShow/hide the overlay
model.submitFunctionCallback function when the overlay submits. Returns the overlay model object
model.closeFunctionCallback function when the overlay closes. Returns a copy of the overlay model object before being modified
+ + +

Content Picker

+Opens a content picker.
+view: contentpicker + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
+ + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectionArrayArray of content objects
+ + +

Icon Picker

+Opens an icon picker.
+view: iconpicker + + + + + + + + + + + + + +
ReturnsTypeDetails
model.iconStringThe icon class
+ +

Item Picker

+Opens an item picker.
+view: itempicker + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
model.availableItemsArrayArray of available items
model.selectedItemsArrayArray of selected items. When passed in the selected items will be filtered from the available items.
model.filterBooleanSet to false to hide the filter
+ + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectedItemObjectThe selected item
+ +

Macro Picker

+Opens a media picker.
+view: macropicker + + + + + + + + + + + + + + + +
ParamTypeDetails
model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
+ + + + + + + + + + + + + + + + + + + + +
ReturnsTypeDetails
model.macroParamsArrayArray of macro params
model.selectedMacroObjectThe selected macro
+ +

Media Picker

+Opens a media picker.
+view: mediapicker + + + + + + + + + + + + + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
model.onlyImagesBooleanOnly display files that have an image file-extension
model.disableFolderSelectBooleanDisable folder selection
+ + + + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectedImagesArrayArray of selected images
+ +

Member Group Picker

+Opens a member group picker.
+view: membergrouppicker + + + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
+ + + + + + + + + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectedMemberGroupStringThe selected member group
model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
+ +

Member Picker

+Opens a member picker.
+view: memberpicker + + + + + + + + + + + + + + + +
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
+ + + + + + + + + + + + + + +
ReturnsTypeDetails
model.selectionArrayArray of selected members/td> +
+ +

YSOD

+Opens an overlay to show a custom YSOD.
+view: ysod + + + + + + + + + + + + + + + +
ParamTypeDetails
model.errorObjectError object
+ +@param {object} model Overlay options. +@param {string} view Path to view or one of the default view names. +@param {string} position The overlay position ("left", "right", "center": "target"). +**/ + +(function() { + 'use strict'; + + function OverlayDirective($timeout, formHelper, overlayHelper, localizationService) { + + function link(scope, el, attr, ctrl) { + + scope.directive = { + enableConfirmButton: false + }; + + var overlayNumber = 0; + var numberOfOverlays = 0; + var isRegistered = false; + + var modelCopy = {}; + + function activate() { + + setView(); + + setButtonText(); + + modelCopy = makeModelCopy(scope.model); + + $timeout(function() { + + if (scope.position === "target") { + setTargetPosition(); + } + + // this has to be done inside a timeout to ensure the destroy + // event on other overlays is run before registering a new one + registerOverlay(); + + setOverlayIndent(); + + }); + + } + + function setView() { + + if (scope.view) { + + if (scope.view.indexOf(".html") === -1) { + var viewAlias = scope.view.toLowerCase(); + scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html"; + } + + } + + } + + function setButtonText() { + if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { + scope.model.closeButtonLabel = localizationService.localize("general_close"); + } + if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { + scope.model.submitButtonLabel = localizationService.localize("general_submit"); + } + } + + function registerOverlay() { + + overlayNumber = overlayHelper.registerOverlay(); + + $(document).bind("keydown.overlay-" + overlayNumber, function(event) { + + if (event.which === 27) { + + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + + if(numberOfOverlays === overlayNumber) { + scope.closeOverLay(); + } + + event.preventDefault(); + } + + if (event.which === 13) { + + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + + if(numberOfOverlays === overlayNumber) { + + var activeElementType = document.activeElement.tagName; + var clickableElements = ["A", "BUTTON"]; + var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); + + if(clickableElements.indexOf(activeElementType) === 0) { + document.activeElement.click(); + event.preventDefault(); + } else if(activeElementType === "TEXTAREA" && !submitOnEnter) { + + + } else { + scope.$apply(function () { + scope.submitForm(scope.model); + }); + event.preventDefault(); + } + + } + + } + + }); + + isRegistered = true; + + } + + function unregisterOverlay() { + + if(isRegistered) { + + overlayHelper.unregisterOverlay(); + + $(document).unbind("keydown.overlay-" + overlayNumber); + + isRegistered = false; + } + + } + + function makeModelCopy(object) { + + var newObject = {}; + + for (var key in object) { + if (key !== "event") { + newObject[key] = angular.copy(object[key]); + } + } + + return newObject; + + } + + function setOverlayIndent() { + + var overlayIndex = overlayNumber - 1; + var indentSize = overlayIndex * 20; + var overlayWidth = el.context.clientWidth; + + el.css('width', overlayWidth - indentSize); + + if(scope.position === "center" || scope.position === "target") { + var overlayTopPosition = el.context.offsetTop; + el.css('top', overlayTopPosition + indentSize); + } + + } + + function setTargetPosition() { + + var container = $("#contentwrapper"); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + + var mousePositionClickX = null; + var mousePositionClickY = null; + var elementHeight = null; + var elementWidth = null; + + var position = { + right: "inherit", + left: "inherit", + top: "inherit", + bottom: "inherit" + }; + + // if mouse click position is know place element with mouse in center + if (scope.model.event && scope.model.event) { + + // click position + mousePositionClickX = scope.model.event.pageX; + mousePositionClickY = scope.model.event.pageY; + + // element size + elementHeight = el.context.clientHeight; + elementWidth = el.context.clientWidth; + + // move element to this position + position.left = mousePositionClickX - (elementWidth / 2); + position.top = mousePositionClickY - (elementHeight / 2); + + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = "inherit"; + } + + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = "inherit"; + } + + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = "inherit"; + } + + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = "inherit"; + } + + el.css(position); + + } + + } + + scope.submitForm = function(model) { + if(scope.model.submit) { + if (formHelper.submitForm({scope: scope})) { + formHelper.resetForm({ scope: scope }); + + if(scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { + scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } else { + unregisterOverlay(); + scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton); + } + + } + } + }; + + scope.cancelConfirmSubmit = function() { + scope.model.confirmSubmit.show = false; + }; + + scope.closeOverLay = function() { + + unregisterOverlay(); + + if (scope.model.close) { + scope.model = modelCopy; + scope.model.close(scope.model); + } else { + scope.model.show = false; + scope.model = null; + } + + }; + + // angular does not support ng-show on custom directives + // width isolated scopes. So we have to make our own. + if (attr.hasOwnProperty("ngShow")) { + scope.$watch("ngShow", function(value) { + if (value) { + el.show(); + activate(); + } else { + unregisterOverlay(); + el.hide(); + } + }); + } else { + activate(); + } + + scope.$on('$destroy', function(){ + unregisterOverlay(); + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay.html', + scope: { + ngShow: "=", + model: "=", + view: "=", + position: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlaybackdrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlaybackdrop.directive.js new file mode 100644 index 0000000000..cfcd7e064b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlaybackdrop.directive.js @@ -0,0 +1,31 @@ +(function() { + 'use strict'; + + function OverlayBackdropDirective(overlayHelper) { + + function link(scope, el, attr, ctrl) { + + scope.numberOfOverlays = 0; + + scope.$watch(function(){ + return overlayHelper.getNumberOfOverlays(); + }, function (newValue) { + scope.numberOfOverlays = newValue; + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay-backdrop.html', + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbOverlayBackdrop', OverlayBackdropDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js similarity index 75% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index d84bb8e24d..adbc3a96a7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -1,30 +1,32 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbProperty -* @restrict E -**/ -angular.module("umbraco.directives") - .directive('umbProperty', function (umbPropEditorHelper) { - return { - scope: { - property: "=" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/directives/umb-property.html', - - //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) { - $scope.property.propertyErrorMessage = errorMsg; - }; - } - }; +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbProperty +* @restrict E +**/ +angular.module("umbraco.directives") + .directive('umbProperty', function (umbPropEditorHelper) { + return { + scope: { + property: "=" + }, + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property.html', + link: function(scope) { + scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? 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) { + $scope.property.propertyErrorMessage = errorMsg; + }; + } + }; }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js new file mode 100644 index 0000000000..5bca761162 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertyeditor.directive.js @@ -0,0 +1,42 @@ +/** +* @ngdoc directive +* @function +* @name umbraco.directives.directive:umbPropertyEditor +* @requires formController +* @restrict E +**/ + +//share property editor directive function +var _umbPropertyEditor = function (umbPropEditorHelper) { + return { + scope: { + model: "=", + isPreValue: "@", + preview: "@" + }, + + require: "^form", + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property-editor.html', + link: function (scope, element, attrs, ctrl) { + + //we need to copy the form controller val to our isolated scope so that + //it get's carried down to the child scopes of this! + //we'll also maintain the current form name. + scope[ctrl.$name] = ctrl; + + if(!scope.model.alias){ + scope.model.alias = Math.random().toString(36).slice(2); + } + + scope.$watch("model.view", function(val){ + scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); + }); + } + }; + }; + +//Preffered is the umb-property-editor as its more explicit - but we keep umb-editor for backwards compat +angular.module("umbraco.directives").directive('umbPropertyEditor', _umbPropertyEditor); +angular.module("umbraco.directives").directive('umbEditor', _umbPropertyEditor); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertygroup.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertygroup.directive.js new file mode 100644 index 0000000000..fd2be97b42 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbpropertygroup.directive.js @@ -0,0 +1,9 @@ +angular.module("umbraco.directives.html") + .directive('umbPropertyGroup', function () { + return { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/property/umb-property-group.html' + }; + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbtab.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtab.directive.js similarity index 61% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbtab.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtab.directive.js index f12544646b..f25909affe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbtab.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtab.directive.js @@ -1,14 +1,14 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbTab +* @name umbraco.directives.directive:umbTab * @restrict E **/ angular.module("umbraco.directives") .directive('umbTab', function ($parse, $timeout) { return { restrict: 'E', - replace: true, + replace: true, transclude: 'true', - templateUrl: 'views/directives/umb-tab.html' + templateUrl: 'views/components/tabs/umb-tab.html' }; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbtabs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/html/umbtabs.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabscontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabscontent.directive.js new file mode 100644 index 0000000000..f567e21b6a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabscontent.directive.js @@ -0,0 +1,25 @@ +(function() { + 'use strict'; + + function UmbTabsContentDirective() { + + function link(scope, el, attr, ctrl) { + + scope.view = attr.view; + + } + + var directive = { + restrict: "E", + replace: true, + transclude: 'true', + templateUrl: "views/components/tabs/umb-tabs-content.html", + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTabsContent', UmbTabsContentDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js new file mode 100644 index 0000000000..890acf3d6f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js @@ -0,0 +1,56 @@ +(function() { + 'use strict'; + + function UmbTabsNavDirective($timeout) { + + function link(scope, el, attr) { + + function activate() { + + $timeout(function () { + + //use bootstrap tabs API to show the first one + el.find("a:first").tab('show'); + + //enable the tab drop + el.tabdrop(); + + }); + + } + + var unbindModelWatch = scope.$watch('model', function(newValue, oldValue){ + + activate(); + + }); + + + scope.$on('$destroy', function () { + + //ensure to destroy tabdrop (unbinds window resize listeners) + el.tabdrop("destroy"); + + unbindModelWatch(); + + }); + + } + + var directive = { + restrict: "E", + replace: true, + templateUrl: "views/components/tabs/umb-tabs-nav.html", + scope: { + model: "=", + tabdrop: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTabsNav', UmbTabsNavDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js similarity index 92% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 6fe20a4f8b..75d0144982 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -31,8 +31,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat var template = '
  • '; template += '
    ' + '
    ' + - '' + - '{{tree.name}}
    ' + + ' {{tree.name}}' + '' + '
    '; template += '
      ' + @@ -47,12 +46,12 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat //flag to track the last loaded section when the tree 'un-loads'. We use this to determine if we should // re-load the tree again. For example, if we hover over 'content' the content tree is shown. Then we hover - // outside of the tree and the tree 'un-loads'. When we re-hover over 'content', we don't want to re-load the + // outside of the tree and the tree 'un-loads'. When we re-hover over 'content', we don't want to re-load the // entire tree again since we already still have it in memory. Of course if the section is different we will // reload it. This saves a lot on processing if someone is navigating in and out of the same section many times // since it saves on data retreival and DOM processing. var lastSection = ""; - + //setup a default internal handler if (!scope.eventhandler) { scope.eventhandler = $({}); @@ -68,7 +67,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat $(scope.eventhandler).trigger(eventName, args); } } - + /** This will deleteAnimations to true after the current digest */ function enableDeleteAnimations() { //do timeout so that it re-enables them after this digest @@ -102,9 +101,9 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat scope.loadChildren(node, true); } }; - - /** - Used to do the tree syncing. If the args.tree is not specified we are assuming it has been + + /** + Used to do the tree syncing. If the args.tree is not specified we are assuming it has been specified previously using the _setActiveTreeType */ scope.eventhandler.syncTree = function(args) { @@ -114,7 +113,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat if (!args.path) { throw "args.path cannot be null"; } - + var deferred = $q.defer(); //this is super complex but seems to be working in other places, here we're listening for our @@ -123,7 +122,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat deferred.resolve(syncArgs); }); - //this should normally be set unless it is being called from legacy + //this should normally be set unless it is being called from legacy // code, so set the active tree type before proceeding. if (args.tree) { loadActiveTree(args.tree); @@ -140,7 +139,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat args.path = _.filter(args.path, function (item) { return (item !== "init" && item !== "-1"); }); - //Once those are filtered we need to check if the current user has a special start node id, + //Once those are filtered we need to check if the current user has a special start node id, // if they do, then we're going to trim the start of the array for anything found from that start node // and previous so that the tree syncs properly. The tree syncs from the top down and if there are parts // of the tree's path in there that don't actually exist in the dom/model then syncing will not work. @@ -162,13 +161,13 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat }); - + return deferred.promise; }; - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to have to set an active tree and then sync, the new API does this in one method by using syncTree. loadChildren is optional but if it is set, it will set the current active tree and load the root node's children - this is synonymous with the legacy refreshTree method - again should not be used @@ -194,12 +193,16 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat } } - + //given a tree alias, this will search the current section tree for the specified tree alias and //set that to the activeTree //NOTE: loadChildren is ONLY used for legacy purposes, do not use this when syncing the tree as it will cause problems // since there will be double request and event handling operations. function loadActiveTree(treeAlias, loadChildren) { + if (!treeAlias) { + return; + } + scope.activeTree = undefined; function doLoad(tree) { @@ -210,7 +213,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat } return false; }); - + if (!scope.activeTree) { throw "Could not find the tree " + treeAlias + ", activeTree has not been set"; } @@ -259,7 +262,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat .then(function(data) { //set the data once we have it scope.tree = data; - + enableDeleteAnimations(); scope.loading = false; @@ -268,7 +271,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat scope.activeTree = scope.tree.root; emitEvent("treeLoaded", { tree: scope.tree }); emitEvent("treeNodeExpanded", { tree: scope.tree, node: scope.tree.root, children: scope.tree.root.children }); - + }, function(reason) { scope.loading = false; notificationsService.error("Tree Error", reason); @@ -290,9 +293,9 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat if (activate === undefined || activate === true) { scope.currentNode = data; } - + emitEvent("treeSynced", { node: data, activate: activate }); - + enableDeleteAnimations(); }); @@ -306,8 +309,8 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat ''; }; - /** method to set the current animation for the node. - * This changes dynamically based on if we are changing sections or just loading normal tree data. + /** method to set the current animation for the node. + * This changes dynamically based on if we are changing sections or just loading normal tree data. * When changing sections we don't want all of the tree-ndoes to do their 'leave' animations. */ scope.animation = function() { @@ -326,7 +329,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat //emit treeNodeExpanding event, if a callback object is set on the tree emitEvent("treeNodeExpanding", { tree: scope.tree, node: node }); - //standardising + //standardising if (!node.children) { node.children = []; } @@ -337,7 +340,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat .then(function(data) { //emit expanded event emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: data }); - + enableDeleteAnimations(); deferred.resolve(data); @@ -346,7 +349,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat else { emitEvent("treeNodeExpanded", { tree: scope.tree, node: node, children: node.children }); node.expanded = true; - + enableDeleteAnimations(); deferred.resolve(node.children); @@ -365,13 +368,13 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat }; /** - Method called when an item is clicked in the tree, this passes the + Method called when an item is clicked in the tree, this passes the DOM element, the tree node object and the original click and emits it as a treeNodeSelect element if there is a callback object defined on the tree */ scope.select = function (n, ev) { - //on tree select we need to remove the current node - + //on tree select we need to remove the current node - // whoever handles this will need to make sure the correct node is selected //reset current node selection scope.currentNode = null; @@ -382,7 +385,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat scope.altSelect = function(n, ev) { emitEvent("treeNodeAltSelect", { element: elem, tree: scope.tree, node: n, event: ev }); }; - + //watch for section changes scope.$watch("section", function(newVal, oldVal) { @@ -404,7 +407,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat lastSection = newVal; } }); - + setupExternalEvents(); loadTree(); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js similarity index 88% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index c46bca74a5..f4fe0db4e6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -34,13 +34,13 @@ 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 '' + '
      ' + @@ -72,14 +72,13 @@ angular.module("umbraco.directives") //set the padding .css("padding-left", (node.level * 20) + "px"); - //remove first 'ins' if there is no children - //show/hide last 'ins' depending on children + //toggle visibility of last 'ins' depending on children + //visibility still ensure the space is "reserved", so both nodes with and without children are aligned. if (!node.hasChildren) { - element.find("ins:first").remove(); - element.find("ins").last().hide(); + element.find("ins").last().css("visibility", "hidden"); } else { - element.find("ins").last().show(); + element.find("ins").last().css("visibility", "visible"); } var icon = element.find("i:first"); @@ -148,8 +147,17 @@ angular.module("umbraco.directives") and emits it as a treeNodeSelect element if there is a callback object defined on the tree */ - scope.select = function(n, ev) { + scope.select = function (n, ev) { + if (ev.ctrlKey || + ev.shiftKey || + ev.metaKey || // apple + (ev.button && ev.button === 1) // middle click, >IE9 + everyone else + ) { + return; + } + emitEvent("treeNodeSelect", { element: element, tree: scope.tree, node: n, event: ev }); + ev.preventDefault(); }; /** diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js similarity index 94% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbtreesearchbox.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js index c9dd185495..a3b2fab00f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreesearchbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js @@ -17,7 +17,7 @@ function treeSearchBox(localizationService, searchService, $q) { }, restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-tree-search-box.html', + templateUrl: 'views/components/tree/umb-tree-search-box.html', link: function (scope, element, attrs, ctrl) { scope.term = ""; @@ -40,7 +40,7 @@ function treeSearchBox(localizationService, searchService, $q) { function performSearch() { if (scope.term) { scope.results = []; - + //a canceler exists, so perform the cancelation operation and reset if (canceler) { console.log("CANCELED!"); @@ -80,7 +80,7 @@ function treeSearchBox(localizationService, searchService, $q) { var searcher = searchService.searchContent; //search if (scope.section === "member") { - searcher = searchService.searchMembers; + searcher = searchService.searchMembers; } else if (scope.section === "media") { searcher = searchService.searchMedia; @@ -88,4 +88,4 @@ function treeSearchBox(localizationService, searchService, $q) { } }; } -angular.module('umbraco.directives').directive("umbTreeSearchBox", treeSearchBox); \ No newline at end of file +angular.module('umbraco.directives').directive("umbTreeSearchBox", treeSearchBox); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreesearchresults.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js similarity index 82% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbtreesearchresults.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js index 9542deb5eb..bc74cbb13b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreesearchresults.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js @@ -13,10 +13,10 @@ function treeSearchResults() { }, restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-tree-search-results.html', + templateUrl: 'views/components/tree/umb-tree-search-results.html', link: function (scope, element, attrs, ctrl) { - + } }; } -angular.module('umbraco.directives').directive("umbTreeSearchResults", treeSearchResults); \ No newline at end of file +angular.module('umbraco.directives').directive("umbTreeSearchResults", treeSearchResults); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js new file mode 100644 index 0000000000..4bc6f08eb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js @@ -0,0 +1,131 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbGenerateAlias +@restrict E +@scope + +@description +Use this directive to generate a camelCased umbraco alias. +When the aliasFrom value is changed the directive will get a formatted alias from the server and update the alias model. If "enableLock" is set to true +the directive will use {@link umbraco.directives.directive:umbLockedField umbLockedField} to lock and unlock the alias. + +

      Markup example

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

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +
      +            vm.name = "";
      +            vm.alias = "";
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +@param {string} alias (binding): The model where the alias is bound. +@param {string} aliasFrom (binding): The model to generate the alias from. +@param {boolean=} enableLock (binding): Set to true to add a lock next to the alias from where it can be unlocked and changed. +**/ + +angular.module("umbraco.directives") + .directive('umbGenerateAlias', function ($timeout, entityResource) { + return { + restrict: 'E', + templateUrl: 'views/components/umb-generate-alias.html', + replace: true, + scope: { + alias: '=', + aliasFrom: '=', + enableLock: '=?', + serverValidationField: '@' + }, + link: function (scope, element, attrs, ctrl) { + + var eventBindings = []; + var bindWatcher = true; + var generateAliasTimeout = ""; + var updateAlias = false; + + scope.locked = true; + scope.placeholderText = "Enter alias..."; + + function generateAlias(value) { + + if (generateAliasTimeout) { + $timeout.cancel(generateAliasTimeout); + } + + if( value !== undefined && value !== "" && value !== null) { + + scope.alias = ""; + scope.placeholderText = "Generating Alias..."; + + generateAliasTimeout = $timeout(function () { + updateAlias = true; + entityResource.getSafeAlias(value, true).then(function (safeAlias) { + if (updateAlias) { + scope.alias = safeAlias.alias; + } + }); + }, 500); + + } else { + updateAlias = true; + scope.alias = ""; + scope.placeholderText = "Enter alias..."; + } + + } + + // if alias gets unlocked - stop watching alias + eventBindings.push(scope.$watch('locked', function(newValue, oldValue){ + if(newValue === false) { + bindWatcher = false; + } + })); + + // validate custom entered alias + eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ + + if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) { + // add watcher + eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) { + if(bindWatcher) { + generateAlias(newValue); + } + })); + } + + })); + + // clean up + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js new file mode 100644 index 0000000000..4fc22c4b74 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js @@ -0,0 +1,222 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbChildSelector +@restrict E +@scope + +@description +Use this directive to render a ui component for selecting child items to a parent node. + +

      Markup example

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

      Controller example

      +
      +	(function () {
      +		"use strict";
      +
      +		function Controller() {
      +
      +            var vm = this;
      +
      +            vm.id = 1;
      +            vm.name = "My Parent element";
      +            vm.icon = "icon-document";
      +            vm.selectedChildren = [];
      +            vm.availableChildren = [
      +                {
      +                    id: 1,
      +                    alias: "item1",
      +                    name: "Item 1",
      +                    icon: "icon-document"
      +                },
      +                {
      +                    id: 2,
      +                    alias: "item2",
      +                    name: "Item 2",
      +                    icon: "icon-document"
      +                }
      +            ];
      +
      +            vm.addChild = addChild;
      +            vm.removeChild = removeChild;
      +
      +            function addChild($event) {
      +                vm.overlay = {
      +                    view: "itempicker",
      +                    title: "Choose child",
      +                    availableItems: vm.availableChildren,
      +                    selectedItems: vm.selectedChildren,
      +                    event: $event,
      +                    show: true,
      +                    submit: function(model) {
      +
      +                        // add selected child
      +                        vm.selectedChildren.push(model.selectedItem);
      +
      +                        // close overlay
      +                        vm.overlay.show = false;
      +                        vm.overlay = null;
      +                    }
      +                };
      +            }
      +
      +            function removeChild($index) {
      +                vm.selectedChildren.splice($index, 1);
      +            }
      +
      +        }
      +
      +		angular.module("umbraco").controller("My.Controller", Controller);
      +
      +	})();
      +
      + +@param {array} selectedChildren (binding): Array of selected children. +@param {array} availableChildren (binding: Array of items available for selection. +@param {string} parentName (binding): The parent name. +@param {string} parentIcon (binding): The parent icon. +@param {number} parentId (binding): The parent id. +@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. +

      The callback returns:

      +
        +
      • child: The selected item.
      • +
      • $index: The selected item index.
      • +
      +@param {callback} onAdd (binding): Callback when the add button is clicked. +

      The callback returns:

      +
        +
      • $event: The select event.
      • +
      +**/ + +(function() { + 'use strict'; + + function ChildSelectorDirective() { + + function link(scope, el, attr, ctrl) { + + var eventBindings = []; + scope.dialogModel = {}; + scope.showDialog = false; + + scope.removeChild = function(selectedChild, $index) { + if(scope.onRemove) { + scope.onRemove(selectedChild, $index); + } + }; + + scope.addChild = function($event) { + if(scope.onAdd) { + scope.onAdd($event); + } + }; + + function syncParentName() { + + // update name on available item + angular.forEach(scope.availableChildren, function(availableChild){ + if(availableChild.id === scope.parentId) { + availableChild.name = scope.parentName; + } + }); + + // update name on selected child + angular.forEach(scope.selectedChildren, function(selectedChild){ + if(selectedChild.id === scope.parentId) { + selectedChild.name = scope.parentName; + } + }); + + } + + function syncParentIcon() { + + // update icon on available item + angular.forEach(scope.availableChildren, function(availableChild){ + if(availableChild.id === scope.parentId) { + availableChild.icon = scope.parentIcon; + } + }); + + // update icon on selected child + angular.forEach(scope.selectedChildren, function(selectedChild){ + if(selectedChild.id === scope.parentId) { + selectedChild.icon = scope.parentIcon; + } + }); + + } + + eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){ + + if (newValue === oldValue) { return; } + if ( oldValue === undefined || newValue === undefined) { return; } + + syncParentName(); + + })); + + eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){ + + if (newValue === oldValue) { return; } + if ( oldValue === undefined || newValue === undefined) { return; } + + syncParentIcon(); + })); + + // clean up + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-child-selector.html', + scope: { + selectedChildren: '=', + availableChildren: "=", + parentName: "=", + parentIcon: "=", + parentId: "=", + onRemove: "=", + onAdd: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbChildSelector', ChildSelectorDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbconfirm.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js similarity index 83% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbconfirm.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js index d1c9015ee3..0c618d9de3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbconfirm.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirm.directive.js @@ -4,22 +4,22 @@ * @function * @description * A confirmation dialog - * + * * @restrict E */ function confirmDirective() { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-confirm.html', + templateUrl: 'views/components/umb-confirm.html', scope: { onConfirm: '=', onCancel: '=', caption: '@' }, link: function (scope, element, attr, ctrl) { - + } }; } -angular.module('umbraco.directives').directive("umbConfirm", confirmDirective); \ No newline at end of file +angular.module('umbraco.directives').directive("umbConfirm", confirmDirective); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js new file mode 100644 index 0000000000..1dcccda481 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js @@ -0,0 +1,105 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbConfirmAction +@restrict E +@scope + +@description +

      Use this directive to toggle a confirmation prompt for an action. +The prompt consists of a checkmark and a cross to confirm or cancel the action. +The prompt can be opened in four direction up, down, left or right.

      + +

      Markup example

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

      Controller example

      +
      +    (function () {
      +
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +            vm.promptIsVisible = false;
      +
      +            vm.confirmAction = confirmAction;
      +            vm.showPrompt = showPrompt;
      +            vm.hidePrompt = hidePrompt;
      +
      +            function confirmAction() {
      +                // confirm logic here
      +            }
      +
      +            function showPrompt() {
      +                vm.promptIsVisible = true;
      +            }
      +
      +            function hidePrompt() {
      +                vm.promptIsVisible = false;
      +            }
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +@param {string} direction The direction the prompt opens ("up", "down", "left", "right"). +@param {callback} onConfirm Callback when the checkmark is clicked. +@param {callback} onCancel Callback when the cross is clicked. +**/ + +(function() { + 'use strict'; + + function ConfirmAction() { + + function link(scope, el, attr, ctrl) { + + scope.clickConfirm = function() { + if(scope.onConfirm) { + scope.onConfirm(); + } + }; + + scope.clickCancel = function() { + if(scope.onCancel) { + scope.onCancel(); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-confirm-action.html', + scope: { + direction: "@", + onConfirm: "&", + onCancel: "&" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbConfirmAction', ConfirmAction); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js new file mode 100644 index 0000000000..3994770c8e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcontentgrid.directive.js @@ -0,0 +1,145 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbContentGrid +@restrict E +@scope + +@description +Use this directive to generate a list of content items presented as a flexbox grid. + +

      Markup example

      +
      +    
      + + + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +            vm.contentItems = [
      +                {
      +                    "name": "Cape",
      +                    "published": true,
      +                    "icon": "icon-document",
      +                    "updateDate": "15-02-2016",
      +                    "owner": "Mr. Batman",
      +                    "selected": false
      +                },
      +                {
      +                    "name": "Utility Belt",
      +                    "published": true,
      +                    "icon": "icon-document",
      +                    "updateDate": "15-02-2016",
      +                    "owner": "Mr. Batman",
      +                    "selected": false
      +                },
      +                {
      +                    "name": "Cave",
      +                    "published": true,
      +                    "icon": "icon-document",
      +                    "updateDate": "15-02-2016",
      +                    "owner": "Mr. Batman",
      +                    "selected": false
      +                }
      +            ];
      +            vm.includeProperties = [
      +                {
      +                  "alias": "updateDate",
      +                  "header": "Last edited"
      +                },
      +                {
      +                  "alias": "owner",
      +                  "header": "Created by"
      +                }
      +            ];
      +
      +            vm.clickItem = clickItem;
      +            vm.selectItem = selectItem;
      +
      +
      +            function clickItem(item, $event, $index){
      +                // do magic here
      +            }
      +
      +            function selectItem(item, $event, $index) {
      +                // set item.selected = true; to select the item
      +                // do magic here
      +            }
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +@param {array} content (binding): Array of content items. +@param {array=} contentProperties (binding): Array of content item properties to include in the item. If left empty the item will only show the item icon and name. +@param {callback=} onClick (binding): Callback method to handle click events on the content item. +

      The callback returns:

      +
        +
      • item: The clicked item
      • +
      • $event: The select event
      • +
      • $index: The item index
      • +
      +@param {callback=} onClickName (binding): Callback method to handle click events on the checkmark icon. +

      The callback returns:

      +
        +
      • item: The selected item
      • +
      • $event: The select event
      • +
      • $index: The item index
      • +
      +**/ + +(function() { + 'use strict'; + + function ContentGridDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickItem = function(item, $event, $index) { + if(scope.onClick) { + scope.onClick(item, $event, $index); + } + }; + + scope.clickItemName = function(item, $event, $index) { + if(scope.onClickName) { + scope.onClickName(item, $event, $index); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-content-grid.html', + scope: { + content: '=', + contentProperties: "=", + onClick: "=", + onClickName: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbContentGrid', ContentGridDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdisableformvalidation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdisableformvalidation.directive.js new file mode 100644 index 0000000000..c101504ea7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdisableformvalidation.directive.js @@ -0,0 +1,20 @@ +(function() { + 'use strict'; + + function UmbDisableFormValidation() { + + var directive = { + restrict: 'A', + require: '?form', + link: function (scope, elm, attrs, ctrl) { + //override the $setValidity function of the form to disable validation + ctrl.$setValidity = function () { }; + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbDisableFormValidation', UmbDisableFormValidation); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js new file mode 100644 index 0000000000..b37895637b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbemptystate.directive.js @@ -0,0 +1,48 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbEmptyState +@restrict E +@scope + +@description +Use this directive to show an empty state message. + +

      Markup example

      +
      +    
      + + + // Empty state content + + +
      +
      + +@param {string=} size Set the size of the text ("small", "large"). +@param {string=} position Set the position of the text ("center"). +**/ + +(function() { + 'use strict'; + + function EmptyStateDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/umb-empty-state.html', + scope: { + size: '@', + position: '@' + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbEmptyState', EmptyStateDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js new file mode 100644 index 0000000000..812038b51a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbfoldergrid.directive.js @@ -0,0 +1,120 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbFolderGrid +@restrict E +@scope + +@description +Use this directive to generate a list of folders presented as a flexbox grid. + +

      Markup example

      +
      +    
      + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller(myService) {
      +
      +            var vm = this;
      +            vm.folders = [
      +                {
      +                    "name": "Folder 1",
      +                    "icon": "icon-folder",
      +                    "selected": false
      +                },
      +                {
      +                    "name": "Folder 2",
      +                    "icon": "icon-folder",
      +                    "selected": false
      +                }
      +
      +            ];
      +
      +            vm.clickFolder = clickFolder;
      +            vm.selectFolder = selectFolder;
      +
      +            myService.getFolders().then(function(folders){
      +                vm.folders = folders;
      +            });
      +
      +            function clickFolder(folder){
      +                // Execute when clicking folder name/link
      +            }
      +
      +            function selectFolder(folder, event, index) {
      +                // Execute when clicking folder
      +                // set folder.selected = true; to show checkmark icon
      +            }
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +@param {array} folders (binding): Array of folders +@param {callback=} onClick (binding): Callback method to handle click events on the folder. +

      The callback returns:

      +
        +
      • folder: The selected folder
      • +
      +@param {callback=} onSelect (binding): Callback method to handle click events on the checkmark icon. +

      The callback returns:

      +
        +
      • folder: The selected folder
      • +
      • $event: The select event
      • +
      • $index: The folder index
      • +
      +**/ + +(function() { + 'use strict'; + + function FolderGridDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickFolder = function(folder, $event, $index) { + if(scope.onClick) { + scope.onClick(folder, $event, $index); + } + }; + + scope.clickFolderName = function(folder, $event, $index) { + if(scope.onClickName) { + scope.onClickName(folder, $event, $index); + $event.stopPropagation(); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-folder-grid.html', + scope: { + folders: '=', + onClick: "=", + onClickName: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbFolderGrid', FolderGridDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js new file mode 100644 index 0000000000..f8bdb50e32 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgridselector.directive.js @@ -0,0 +1,157 @@ +(function() { + 'use strict'; + + function GridSelector() { + + function link(scope, el, attr, ctrl) { + + var eventBindings = []; + scope.dialogModel = {}; + scope.showDialog = false; + scope.itemLabel = ""; + + // set default item name + if(!scope.itemName){ + scope.itemLabel = "item"; + } else { + scope.itemLabel = scope.itemName; + } + + scope.removeItem = function(selectedItem) { + var selectedItemIndex = scope.selectedItems.indexOf(selectedItem); + scope.selectedItems.splice(selectedItemIndex, 1); + }; + + scope.removeDefaultItem = function() { + + // it will be the last item so we can clear the array + scope.selectedItems = []; + + // remove as default item + scope.defaultItem = null; + + }; + + scope.openItemPicker = function($event){ + scope.dialogModel = { + view: "itempicker", + title: "Choose " + scope.itemLabel, + availableItems: scope.availableItems, + selectedItems: scope.selectedItems, + event: $event, + show: true, + submit: function(model) { + scope.selectedItems.push(model.selectedItem); + + // if no default item - set item as default + if(scope.defaultItem === null) { + scope.setAsDefaultItem(model.selectedItem); + } + + scope.dialogModel.show = false; + scope.dialogModel = null; + } + }; + }; + + scope.setAsDefaultItem = function(selectedItem) { + + // clear default item + scope.defaultItem = {}; + + // set as default item + scope.defaultItem = selectedItem; + }; + + function updatePlaceholders() { + + // update default item + if(scope.defaultItem !== null && scope.defaultItem.placeholder) { + + scope.defaultItem.name = scope.name; + + if(scope.alias !== null && scope.alias !== undefined) { + scope.defaultItem.alias = scope.alias; + } + + } + + // update selected items + angular.forEach(scope.selectedItems, function(selectedItem) { + if(selectedItem.placeholder) { + + selectedItem.name = scope.name; + + if(scope.alias !== null && scope.alias !== undefined) { + selectedItem.alias = scope.alias; + } + + } + }); + + // update availableItems + angular.forEach(scope.availableItems, function(availableItem) { + if(availableItem.placeholder) { + + availableItem.name = scope.name; + + if(scope.alias !== null && scope.alias !== undefined) { + availableItem.alias = scope.alias; + } + + } + }); + + } + + function activate() { + + // add watchers for updating placeholde name and alias + if(scope.updatePlaceholder) { + eventBindings.push(scope.$watch('name', function(newValue, oldValue){ + updatePlaceholders(); + })); + + eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ + updatePlaceholders(); + })); + } + + } + + activate(); + + // clean up + scope.$on('$destroy', function(){ + + // clear watchers + for(var e in eventBindings) { + eventBindings[e](); + } + + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-grid-selector.html', + scope: { + name: "=", + alias: "=", + selectedItems: '=', + availableItems: "=", + defaultItem: "=", + itemName: "@", + updatePlaceholder: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbGridSelector', GridSelector); + +})(); 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 new file mode 100644 index 0000000000..caa79439be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -0,0 +1,667 @@ +(function() { + 'use strict'; + + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService) { + + function link(scope, el, attr, ctrl) { + + var validationTranslated = ""; + var tabNoSortOrderTranslated = ""; + + scope.sortingMode = false; + scope.toolbar = []; + scope.sortableOptionsGroup = {}; + scope.sortableOptionsProperty = {}; + scope.sortingButtonKey = "general_reorder"; + + function activate() { + + setSortingOptions(); + + // set placeholder property on each group + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function(group) { + addInitProperty(group); + }); + } + + // add init tab + addInitGroup(scope.model.groups); + + activateFirstGroup(scope.model.groups); + + // localize texts + localizationService.localize("validation_validation").then(function(value) { + validationTranslated = value; + }); + + localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) { + tabNoSortOrderTranslated = value; + }); + } + + function setSortingOptions() { + + scope.sortableOptionsGroup = { + distance: 10, + tolerance: "pointer", + opacity: 0.7, + scroll: true, + cursor: "move", + placeholder: "umb-group-builder__group-sortable-placeholder", + zIndex: 6000, + handle: ".umb-group-builder__group-handle", + items: ".umb-group-builder__group-sortable", + start: function(e, ui) { + ui.placeholder.height(ui.item.height()); + }, + stop: function(e, ui) { + updateTabsSortOrder(); + }, + }; + + scope.sortableOptionsProperty = { + distance: 10, + tolerance: "pointer", + connectWith: ".umb-group-builder__properties", + opacity: 0.7, + scroll: true, + cursor: "move", + placeholder: "umb-group-builder__property_sortable-placeholder", + zIndex: 6000, + handle: ".umb-group-builder__property-handle", + items: ".umb-group-builder__property-sortable", + start: function(e, ui) { + ui.placeholder.height(ui.item.height()); + }, + stop: function(e, ui) { + updatePropertiesSortOrder(); + } + }; + + } + + function updateTabsSortOrder() { + + var first = true; + var prevSortOrder = 0; + + scope.model.groups.map(function(group){ + + var index = scope.model.groups.indexOf(group); + + if(group.tabState !== "init") { + + // set the first not inherited tab to sort order 0 + if(!group.inherited && first) { + + // set the first tab sort order to 0 if prev is 0 + if( prevSortOrder === 0 ) { + group.sortOrder = 0; + // when the first tab is inherited and sort order is not 0 + } else { + group.sortOrder = prevSortOrder + 1; + } + + first = false; + + } else if(!group.inherited && !first) { + + // find next group + var nextGroup = scope.model.groups[index + 1]; + + // if a groups is dropped in the middle of to groups with + // same sort order. Give it the dropped group same sort order + if( prevSortOrder === nextGroup.sortOrder ) { + group.sortOrder = prevSortOrder; + } else { + group.sortOrder = prevSortOrder + 1; + } + + } + + // store this tabs sort order as reference for the next + prevSortOrder = group.sortOrder; + + } + + }); + + } + + function filterAvailableCompositions(selectedContentType, selecting) { + + //selecting = true if the user has check the item, false if the user has unchecked the item + + var selectedContentTypeAliases = selecting ? + //the user has selected the item so add to the current list + _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : + //the user has unselected the item so remove from the current list + _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { + return i === selectedContentType.alias; + }); + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; + }); + + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + + return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { + //reset first + current.allowed = true; + //see if this list item is found in the response (allowed) list + var found = _.find(filteredAvailableCompositeTypes, function (f) { + return current.contentType.alias === f.contentType.alias; + }); + + //allow if the item was found in the response (allowed) list - + // and ensure its set to allowed if it is currently checked, + // DO not allow if it's a locked content type. + current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && + (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false); + + }); + }); + } + + function updatePropertiesSortOrder() { + + angular.forEach(scope.model.groups, function(group){ + if( group.tabState !== "init" ) { + group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); + } + }); + + } + + function setupAvailableContentTypesModel(result) { + scope.compositionsDialogModel.availableCompositeContentTypes = result; + //iterate each one and set it up + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { + //enable it if it's part of the selected model + if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { + c.allowed = true; + } + + //set the inherited flags + c.inherited = false; + if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { + c.inherited = true; + } + // convert icons for composite content types + iconHelper.formatContentTypeIcons([c.contentType]); + }); + } + + /* ---------- DELETE PROMT ---------- */ + + scope.togglePrompt = function (object) { + object.deletePrompt = !object.deletePrompt; + }; + + scope.hidePrompt = function (object) { + object.deletePrompt = false; + }; + + /* ---------- TOOLBAR ---------- */ + + scope.toggleSortingMode = function(tool) { + + if (scope.sortingMode === true) { + + var sortOrderMissing = false; + + for (var i = 0; i < scope.model.groups.length; i++) { + var group = scope.model.groups[i]; + if (group.tabState !== "init" && group.sortOrder === undefined) { + sortOrderMissing = true; + group.showSortOrderMissing = true; + notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated); + } + } + + if (!sortOrderMissing) { + scope.sortingMode = false; + scope.sortingButtonKey = "general_reorder"; + } + + } else { + + scope.sortingMode = true; + scope.sortingButtonKey = "general_reorderDone"; + + } + + }; + + scope.openCompositionsDialog = function() { + + scope.compositionsDialogModel = { + title: "Compositions", + contentType: scope.model, + compositeContentTypes: scope.model.compositeContentTypes, + view: "views/common/overlays/contenttypeeditor/compositions/compositions.html", + confirmSubmit: { + title: "Warning", + description: "Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?", + checkboxLabel: "I know what I'm doing", + enable: true + }, + submit: function(model, oldModel, confirmed) { + + var compositionRemoved = false; + + // check if any compositions has been removed + for(var i = 0; oldModel.compositeContentTypes.length > i; i++) { + + var oldComposition = oldModel.compositeContentTypes[i]; + + if(_.contains(model.compositeContentTypes, oldComposition) === false) { + compositionRemoved = true; + } + + } + + // show overlay confirm box if compositions has been removed. + if(compositionRemoved && confirmed === false) { + + scope.compositionsDialogModel.confirmSubmit.show = true; + + // submit overlay if no compositions has been removed + // or the action has been confirmed + } else { + + // make sure that all tabs has an init property + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function(group) { + addInitProperty(group); + }); + } + + // remove overlay + scope.compositionsDialogModel.show = false; + scope.compositionsDialogModel = null; + } + + }, + close: function(oldModel) { + + // reset composition changes + scope.model.groups = oldModel.contentType.groups; + scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; + + // remove overlay + scope.compositionsDialogModel.show = false; + scope.compositionsDialogModel = null; + + }, + selectCompositeContentType: function (selectedContentType) { + + //first check if this is a new selection - we need to store this value here before any further digests/async + // because after that the scope.model.compositeContentTypes will be populated with the selected value. + var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; + + if (newSelection) { + //merge composition with content type + + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; + + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice( + scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.allowed = false; + } + else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } + + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + }); + } + else { + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); + + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + } + + } + }; + + var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + $q.all([ + //get available composite types + availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { + setupAvailableContentTypesModel(result); + }), + //get content type count + countContentTypeResource().then(function(result) { + scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); + }) + ]).then(function() { + //resolves when both other promises are done, now show it + scope.compositionsDialogModel.show = true; + }); + + }; + + + /* ---------- GROUPS ---------- */ + + scope.addGroup = function(group) { + + // set group sort order + var index = scope.model.groups.indexOf(group); + var prevGroup = scope.model.groups[index - 1]; + + if( index > 0) { + // set index to 1 higher than the previous groups sort order + group.sortOrder = prevGroup.sortOrder + 1; + + } else { + // first group - sort order will be 0 + group.sortOrder = 0; + } + + // activate group + scope.activateGroup(group); + + }; + + scope.activateGroup = function(selectedGroup) { + + // set all other groups that are inactive to active + angular.forEach(scope.model.groups, function(group) { + // skip init tab + if (group.tabState !== "init") { + group.tabState = "inActive"; + } + }); + + selectedGroup.tabState = "active"; + + }; + + scope.removeGroup = function(groupIndex) { + scope.model.groups.splice(groupIndex, 1); + addInitGroup(scope.model.groups); + }; + + scope.updateGroupTitle = function(group) { + if (group.properties.length === 0) { + addInitProperty(group); + } + }; + + scope.changeSortOrderValue = function(group) { + + if (group.sortOrder !== undefined) { + group.showSortOrderMissing = false; + } + scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); + }; + + function addInitGroup(groups) { + + // check i init tab already exists + var addGroup = true; + + angular.forEach(groups, function(group) { + if (group.tabState === "init") { + addGroup = false; + } + }); + + if (addGroup) { + groups.push({ + properties: [], + parentTabContentTypes: [], + parentTabContentTypeNames: [], + name: "", + tabState: "init" + }); + } + + return groups; + } + + function activateFirstGroup(groups) { + if (groups && groups.length > 0) { + var firstGroup = groups[0]; + if(!firstGroup.tabState || firstGroup.tabState === "inActive") { + firstGroup.tabState = "active"; + } + } + } + + /* ---------- PROPERTIES ---------- */ + + scope.addProperty = function(property, group) { + + // set property sort order + var index = group.properties.indexOf(property); + var prevProperty = group.properties[index - 1]; + + if( index > 0) { + // set index to 1 higher than the previous property sort order + property.sortOrder = prevProperty.sortOrder + 1; + + } else { + // first property - sort order will be 0 + property.sortOrder = 0; + } + + // open property settings dialog + scope.editPropertyTypeSettings(property, group); + + }; + + scope.editPropertyTypeSettings = function(property, group) { + + if (!property.inherited && !property.locked) { + + scope.propertySettingsDialogModel = {}; + scope.propertySettingsDialogModel.title = "Property settings"; + scope.propertySettingsDialogModel.property = property; + scope.propertySettingsDialogModel.contentType = scope.contentType; + scope.propertySettingsDialogModel.contentTypeName = scope.model.name; + scope.propertySettingsDialogModel.view = "views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html"; + scope.propertySettingsDialogModel.show = true; + + // set state to active to access the preview + property.propertyState = "active"; + + // set property states + property.dialogIsOpen = true; + + scope.propertySettingsDialogModel.submit = function(model) { + + property.inherited = false; + property.dialogIsOpen = false; + + // update existing data types + if(model.updateSameDataTypes) { + updateSameDataTypes(property); + } + + // remove dialog + scope.propertySettingsDialogModel.show = false; + scope.propertySettingsDialogModel = null; + + // push new init property to group + addInitProperty(group); + + // set focus on init property + var numberOfProperties = group.properties.length; + group.properties[numberOfProperties - 1].focus = true; + + // push new init tab to the scope + addInitGroup(scope.model.groups); + + }; + + scope.propertySettingsDialogModel.close = function(oldModel) { + + // reset all property changes + property.label = oldModel.property.label; + property.alias = oldModel.property.alias; + property.description = oldModel.property.description; + property.config = oldModel.property.config; + property.editor = oldModel.property.editor; + property.view = oldModel.property.view; + property.dataTypeId = oldModel.property.dataTypeId; + property.dataTypeIcon = oldModel.property.dataTypeIcon; + property.dataTypeName = oldModel.property.dataTypeName; + property.validation.mandatory = oldModel.property.validation.mandatory; + property.validation.pattern = oldModel.property.validation.pattern; + property.showOnMemberProfile = oldModel.property.showOnMemberProfile; + property.memberCanEdit = oldModel.property.memberCanEdit; + + // 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 + if(oldModel.property.editor === undefined || oldModel.property.editor === null || oldModel.property.editor === "") { + property.propertyState = "init"; + } else { + property.propertyState = oldModel.property.propertyState; + } + + // remove dialog + scope.propertySettingsDialogModel.show = false; + scope.propertySettingsDialogModel = null; + + }; + + } + }; + + scope.deleteProperty = function(tab, propertyIndex) { + + // remove property + tab.properties.splice(propertyIndex, 1); + + // if the last property in group is an placeholder - remove add new tab placeholder + if(tab.properties.length === 1 && tab.properties[0].propertyState === "init") { + + angular.forEach(scope.model.groups, function(group, index, groups){ + if(group.tabState === 'init') { + groups.splice(index, 1); + } + }); + + } + + }; + + function addInitProperty(group) { + + var addInitPropertyBool = true; + var initProperty = { + label: null, + alias: null, + propertyState: "init", + validation: { + mandatory: false, + pattern: null + } + }; + + // check if there already is an init property + angular.forEach(group.properties, function(property) { + if (property.propertyState === "init") { + addInitPropertyBool = false; + } + }); + + if (addInitPropertyBool) { + group.properties.push(initProperty); + } + + return group; + } + + function updateSameDataTypes(newProperty) { + + // find each property + angular.forEach(scope.model.groups, function(group){ + angular.forEach(group.properties, function(property){ + + if(property.dataTypeId === newProperty.dataTypeId) { + + // update property data + property.config = newProperty.config; + property.editor = newProperty.editor; + property.view = newProperty.view; + property.dataTypeId = newProperty.dataTypeId; + property.dataTypeIcon = newProperty.dataTypeIcon; + property.dataTypeName = newProperty.dataTypeName; + + } + + }); + }); + } + + + var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) { + if (newValue !== undefined && newValue.groups !== undefined) { + activate(); + } + }); + + // clean up + scope.$on('$destroy', function(){ + unbindModelWatcher(); + }); + + } + + var directive = { + restrict: "E", + replace: true, + templateUrl: "views/components/umb-groups-builder.html", + scope: { + model: "=", + compositions: "=", + sorting: "=", + contentType: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js new file mode 100644 index 0000000000..1bbfb850d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js @@ -0,0 +1,140 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbkeyboardShortcutsOverview +@restrict E +@scope + +@description + +

      Use this directive to show an overview of keyboard shortcuts in an editor. +The directive will render an overview trigger wich shows how the overview is opened. +When this combination is hit an overview is opened with shortcuts based on the model sent to the directive.

      + +

      Markup example

      +
      +    
      + + + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +
      +            vm.keyboardShortcutsOverview = [
      +                {
      +                    "name": "Sections",
      +                    "shortcuts": [
      +                        {
      +                            "description": "Navigate sections",
      +                            "keys": [
      +                                {"key": "1"},
      +                                {"key": "4"}
      +                            ],
      +                            "keyRange": true
      +                        }
      +                    ]
      +                },
      +                {
      +                    "name": "Design",
      +                    "shortcuts": [
      +                        {
      +                            "description": "Add tab",
      +                            "keys": [
      +                                {"key": "alt"},
      +                                {"key": "shift"},
      +                                {"key": "t"}
      +                            ]
      +                        }
      +                    ]
      +                }
      +            ];
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +

      Model description

      +
        +
      • + name + (string) - + Sets the shortcut section name. +
      • +
      • + shortcuts + (array) - + Array of available shortcuts in the section. +
      • +
          +
        • + description + (string) - + Short description of the shortcut. +
        • +
        • + keys + (array) - + Array of keys in the shortcut. +
        • +
            +
          • + key + (string) - + The invidual key in the shortcut. +
          • +
          +
        • + keyRange + (boolean) - + Set to true to show a key range. It combines the shortcut keys with "-" instead of "+". +
        • +
        +
      + +@param {object} model keyboard shortcut model. See description and example above. +**/ + +(function() { + 'use strict'; + + function KeyboardShortcutsOverviewDirective() { + + function link(scope, el, attr, ctrl) { + + scope.shortcutOverlay = false; + + scope.toggleShortcutsOverlay = function() { + scope.shortcutOverlay = !scope.shortcutOverlay; + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-keyboard-shortcuts-overview.html', + link: link, + scope: { + model: "=" + } + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbKeyboardShortcutsOverview', KeyboardShortcutsOverviewDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umblaunchminieditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblaunchminieditor.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/editors/umblaunchminieditor.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/umblaunchminieditor.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js new file mode 100644 index 0000000000..48291bfa87 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js @@ -0,0 +1,93 @@ +(function() { + 'use strict'; + + function LayoutSelectorDirective() { + + function link(scope, el, attr, ctrl) { + + scope.layoutDropDownIsOpen = false; + scope.showLayoutSelector = true; + + function activate() { + + setVisibility(); + + setActiveLayout(scope.layouts); + + } + + function setVisibility() { + + var numberOfAllowedLayouts = getNumberOfAllowedLayouts(scope.layouts); + + if(numberOfAllowedLayouts === 1) { + scope.showLayoutSelector = false; + } + + } + + function getNumberOfAllowedLayouts(layouts) { + + var allowedLayouts = 0; + + for (var i = 0; layouts.length > i; i++) { + + var layout = layouts[i]; + + if(layout.selected === true) { + allowedLayouts++; + } + + } + + return allowedLayouts; + } + + function setActiveLayout(layouts) { + + for (var i = 0; layouts.length > i; i++) { + var layout = layouts[i]; + if(layout.path === scope.activeLayout.path) { + layout.active = true; + } + } + + } + + scope.pickLayout = function(selectedLayout) { + if(scope.onLayoutSelect) { + scope.onLayoutSelect(selectedLayout); + scope.layoutDropDownIsOpen = false; + } + }; + + scope.toggleLayoutDropdown = function() { + scope.layoutDropDownIsOpen = !scope.layoutDropDownIsOpen; + }; + + scope.closeLayoutDropdown = function() { + scope.layoutDropDownIsOpen = false; + }; + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-layout-selector.html', + scope: { + layouts: '=', + activeLayout: '=', + onLayoutSelect: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLayoutSelector', LayoutSelectorDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewlayout.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewlayout.directive.js new file mode 100644 index 0000000000..d026a3f4e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewlayout.directive.js @@ -0,0 +1,37 @@ +(function() { + 'use strict'; + + function ListViewLayoutDirective() { + + function link(scope, el, attr, ctrl) { + + scope.getContent = function(contentId) { + if(scope.onGetContent) { + scope.onGetContent(contentId); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-list-view-layout.html', + scope: { + contentId: '=', + folders: '=', + items: '=', + selection: '=', + options: '=', + entityType: '@', + onGetContent: '=' + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbListViewLayout', ListViewLayoutDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js new file mode 100644 index 0000000000..ae9da02237 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js @@ -0,0 +1,153 @@ +(function() { + 'use strict'; + + function ListViewSettingsDirective(contentTypeResource, dataTypeResource, dataTypeHelper) { + + function link(scope, el, attr, ctrl) { + + scope.dataType = {}; + scope.editDataTypeSettings = false; + scope.customListViewCreated = false; + + /* ---------- INIT ---------- */ + + function activate() { + + if(scope.enableListView) { + + dataTypeResource.getByName(scope.listViewName) + .then(function(dataType) { + + scope.dataType = dataType; + + scope.customListViewCreated = checkForCustomListView(); + + }); + + } else { + + scope.dataType = {}; + + } + + } + + /* ----------- LIST VIEW SETTINGS --------- */ + + scope.toggleEditListViewDataTypeSettings = function() { + scope.editDataTypeSettings = !scope.editDataTypeSettings; + }; + + scope.saveListViewDataType = function() { + + var preValues = dataTypeHelper.createPreValueProps(scope.dataType.preValues); + + dataTypeResource.save(scope.dataType, preValues, false).then(function(dataType) { + + // store data type + scope.dataType = dataType; + + // hide settings panel + scope.editDataTypeSettings = false; + + }); + + }; + + + /* ---------- CUSTOM LIST VIEW ---------- */ + + scope.createCustomListViewDataType = function() { + + dataTypeResource.createCustomListView(scope.modelAlias).then(function(dataType) { + + // store data type + scope.dataType = dataType; + + // set list view name on scope + scope.listViewName = dataType.name; + + // change state to custom list view + scope.customListViewCreated = true; + + // show settings panel + scope.editDataTypeSettings = true; + + }); + + }; + + scope.removeCustomListDataType = function() { + + scope.editDataTypeSettings = false; + + // delete custom list view data type + dataTypeResource.deleteById(scope.dataType.id).then(function(dataType) { + + // set list view name on scope + if(scope.contentType === "documentType") { + + scope.listViewName = "List View - Content"; + + } else if(scope.contentType === "mediaType") { + + scope.listViewName = "List View - Media"; + + } + + // get default data type + dataTypeResource.getByName(scope.listViewName) + .then(function(dataType) { + + // store data type + scope.dataType = dataType; + + // change state to default list view + scope.customListViewCreated = false; + + }); + }); + + }; + + /* ----------- SCOPE WATCHERS ----------- */ + var unbindEnableListViewWatcher = scope.$watch('enableListView', function(newValue, oldValue){ + + if(newValue !== undefined) { + activate(); + } + + }); + + // clean up + scope.$on('$destroy', function(){ + unbindEnableListViewWatcher(); + }); + + /* ----------- METHODS ---------- */ + + function checkForCustomListView() { + return scope.dataType.name === "List View - " + scope.modelAlias; + } + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-list-view-settings.html', + scope: { + enableListView: "=", + listViewName: "=", + modelAlias: "=", + contentType: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbListViewSettings', ListViewSettingsDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js new file mode 100644 index 0000000000..0671770796 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js @@ -0,0 +1,64 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbLoadIndicator +@restrict E + +@description +Use this directive to generate a loading indicator. + +

      Markup example

      +
      +    
      + + + + +
      +

      {{content}}

      +
      + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller(myService) {
      +
      +            var vm = this;
      +
      +            vm.content = "";
      +            vm.loading = true;
      +
      +            myService.getContent().then(function(content){
      +                vm.content = content;
      +                vm.loading = false;
      +            });
      +
      +        }
      +½
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      +**/ + +(function() { + 'use strict'; + + function UmbLoadIndicatorDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-load-indicator.html' + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLoadIndicator', UmbLoadIndicatorDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js new file mode 100644 index 0000000000..cc5a1eb2b1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js @@ -0,0 +1,110 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbLockedField +@restrict E +@scope + +@description +Use this directive to render a value with a lock next to it. When the lock is clicked the value gets unlocked and can be edited. + +

      Markup example

      +
      +	
      + + + + +
      +
      + +

      Controller example

      +
      +	(function () {
      +		"use strict";
      +
      +		function Controller() {
      +
      +			var vm = this;
      +			vm.value = "My locked text";
      +
      +        }
      +
      +		angular.module("umbraco").controller("My.Controller", Controller);
      +
      +	})();
      +
      + +@param {string} ngModel (binding): The locked text. +@param {boolean=} locked (binding): true by default. Set to false to unlock the text. +@param {string=} placeholderText (binding): If ngModel is empty this text will be shown. +@param {string=} regexValidation (binding): Set a regex expression for validation of the field. +@param {string=} serverValidationField (attribute): Set a server validation field. +**/ + +(function() { + 'use strict'; + + function LockedFieldDirective($timeout, localizationService) { + + function link(scope, el, attr, ngModelCtrl) { + + function activate() { + + // if locked state is not defined as an attr set default state + if (scope.locked === undefined || scope.locked === null) { + scope.locked = true; + } + + // if regex validation is not defined as an attr set default state + // if this is set to an empty string then regex validation can be ignored. + if (scope.regexValidation === undefined || scope.regexValidation === null) { + scope.regexValidation = "^[a-zA-Z]\\w.*$"; + } + + if (scope.serverValidationField === undefined || scope.serverValidationField === null) { + scope.serverValidationField = ""; + } + + // if locked state is not defined as an attr set default state + if (scope.placeholderText === undefined || scope.placeholderText === null) { + scope.placeholderText = "Enter value..."; + } + + } + + scope.lock = function() { + scope.locked = true; + }; + + scope.unlock = function() { + scope.locked = false; + }; + + activate(); + + } + + var directive = { + require: "ngModel", + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-locked-field.html', + scope: { + ngModel: "=", + locked: "=?", + placeholderText: "=?", + regexValidation: "=?", + serverValidationField: "@" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbLockedField', LockedFieldDirective); + +})(); 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 new file mode 100644 index 0000000000..7d2da34988 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -0,0 +1,286 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbMediaGrid +@restrict E +@scope + +@description +Use this directive to generate a thumbnail grid of media items. + +

      Markup example

      +
      +    
      + + + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +            vm.mediaItems = [];
      +
      +            vm.clickItem = clickItem;
      +            vm.clickItemName = clickItemName;
      +
      +            myService.getMediaItems().then(function (mediaItems) {
      +                vm.mediaItems = mediaItems;
      +            });
      +
      +            function clickItem(item, $event, $index){
      +                // do magic here
      +            }
      +
      +            function clickItemName(item, $event, $index) {
      +                // set item.selected = true; to select the item
      +                // do magic here
      +            }
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +@param {array} items (binding): Array of media items. +@param {callback=} onDetailsHover (binding): Callback method when the details icon is hovered. +

      The callback returns:

      +
        +
      • item: The hovered item
      • +
      • $event: The hover event
      • +
      • hover: Boolean to tell if the item is hovered or not
      • +
      +@param {callback=} onClick (binding): Callback method to handle click events on the media item. +

      The callback returns:

      +
        +
      • item: The clicked item
      • +
      • $event: The click event
      • +
      • $index: The item index
      • +
      +@param {callback=} onClickName (binding): Callback method to handle click events on the media item name. +

      The callback returns:

      +
        +
      • item: The clicked item
      • +
      • $event: The click event
      • +
      • $index: The item index
      • +
      +@param {string=} filterBy (binding): String to filter media items by +@param {string=} itemMaxWidth (attribute): Sets a max width on the media item thumbnails. +@param {string=} itemMaxHeight (attribute): Sets a max height on the media item thumbnails. +@param {string=} itemMinWidth (attribute): Sets a min width on the media item thumbnails. +@param {string=} itemMinHeight (attribute): Sets a min height on the media item thumbnails. + +**/ + +(function() { + 'use strict'; + + function MediaGridDirective($filter, mediaHelper) { + + function link(scope, el, attr, ctrl) { + + var itemDefaultHeight = 200; + var itemDefaultWidth = 200; + var itemMaxWidth = 200; + var itemMaxHeight = 200; + var itemMinWidth = 125; + var itemMinHeight = 125; + + function activate() { + + if (scope.itemMaxWidth) { + itemMaxWidth = scope.itemMaxWidth; + } + + if (scope.itemMaxHeight) { + itemMaxHeight = scope.itemMaxHeight; + } + + if (scope.itemMinWidth) { + itemMinWidth = scope.itemMinWidth; + } + + if (scope.itemMinWidth) { + itemMinHeight = scope.itemMinHeight; + } + + for (var i = 0; scope.items.length > i; i++) { + var item = scope.items[i]; + setItemData(item); + setOriginalSize(item, itemMaxHeight); + + // remove non images when onlyImages is set to true + if(scope.onlyImages === "true" && !item.isFolder && !item.thumbnail){ + scope.items.splice(i, 1); + i--; + } + + } + + if (scope.items.length > 0) { + setFlexValues(scope.items); + } + + } + + function setItemData(item) { + item.isFolder = !mediaHelper.hasFilePropertyType(item); + if (!item.isFolder) { + item.thumbnail = mediaHelper.resolveFile(item, true); + item.image = mediaHelper.resolveFile(item, false); + } + } + + function setOriginalSize(item, maxHeight) { + + //set to a square by default + item.width = itemDefaultWidth; + item.height = itemDefaultHeight; + item.aspectRatio = 1; + + var widthProp = _.find(item.properties, function(v) { + return (v.alias === "umbracoWidth"); + }); + + if (widthProp && widthProp.value) { + item.width = parseInt(widthProp.value, 10); + if (isNaN(item.width)) { + item.width = itemDefaultWidth; + } + } + + var heightProp = _.find(item.properties, function(v) { + return (v.alias === "umbracoHeight"); + }); + + if (heightProp && heightProp.value) { + item.height = parseInt(heightProp.value, 10); + if (isNaN(item.height)) { + item.height = itemDefaultWidth; + } + } + + item.aspectRatio = item.width / item.height; + + // set max width and height + // landscape + if (item.aspectRatio >= 1) { + if (item.width > itemMaxWidth) { + item.width = itemMaxWidth; + item.height = itemMaxWidth / item.aspectRatio; + } + // portrait + } else { + if (item.height > itemMaxHeight) { + item.height = itemMaxHeight; + item.width = itemMaxHeight * item.aspectRatio; + } + } + + } + + function setFlexValues(mediaItems) { + + var flexSortArray = mediaItems; + var smallestImageWidth = null; + var widestImageAspectRatio = null; + + // sort array after image width with the widest image first + flexSortArray = $filter('orderBy')(flexSortArray, 'width', true); + + // find widest image aspect ratio + widestImageAspectRatio = flexSortArray[0].aspectRatio; + + // find smallest image width + smallestImageWidth = flexSortArray[flexSortArray.length - 1].width; + + for (var i = 0; flexSortArray.length > i; i++) { + + var mediaItem = flexSortArray[i]; + var flex = 1 / (widestImageAspectRatio / mediaItem.aspectRatio); + + if (flex === 0) { + flex = 1; + } + + var imageMinFlexWidth = smallestImageWidth * flex; + + var flexStyle = { + "flex": flex + " 1 " + imageMinFlexWidth + "px", + "max-width": mediaItem.width + "px", + "min-width": itemMinWidth + "px", + "min-height": itemMinHeight + "px" + }; + + mediaItem.flexStyle = flexStyle; + + } + + } + + scope.clickItem = function(item, $event, $index) { + if (scope.onClick) { + scope.onClick(item, $event, $index); + } + }; + + scope.clickItemName = function(item, $event, $index) { + if (scope.onClickName) { + scope.onClickName(item, $event, $index); + $event.stopPropagation(); + } + }; + + scope.hoverItemDetails = function(item, $event, hover) { + if (scope.onDetailsHover) { + scope.onDetailsHover(item, $event, hover); + } + }; + + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { + if (angular.isArray(newValue)) { + activate(); + } + }); + + scope.$on('$destroy', function() { + unbindItemsWatcher(); + }); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-media-grid.html', + scope: { + items: '=', + onDetailsHover: "=", + onClick: '=', + onClickName: "=", + filterBy: "=", + itemMaxWidth: "@", + itemMaxHeight: "@", + itemMinWidth: "@", + itemMinHeight: "@", + onlyImages: "@" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbMediaGrid', MediaGridDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js new file mode 100644 index 0000000000..fe753171e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -0,0 +1,190 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbPagination +@restrict E +@scope + +@description +Use this directive to generate a pagination. + +

      Markup example

      +
      +    
      + + + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +
      +            vm.pagination = {
      +                pageNumber: 1,
      +                totalPages: 10
      +            }
      +
      +            vm.nextPage = nextPage;
      +            vm.prevPage = prevPage;
      +            vm.goToPage = goToPage;
      +
      +            function nextPage(pageNumber) {
      +                // do magic here
      +                console.log(pageNumber);
      +                alert("nextpage");
      +            }
      +
      +            function prevPage(pageNumber) {
      +                // do magic here
      +                console.log(pageNumber);
      +                alert("prevpage");
      +            }
      +
      +            function goToPage(pageNumber) {
      +                // do magic here
      +                console.log(pageNumber);
      +                alert("go to");
      +            }
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +    })();
      +
      + +@param {number} pageNumber (binding): Current page number. +@param {number} totalPages (binding): The total number of pages. +@param {callback} onNext (binding): Callback method to go to the next page. +

      The callback returns:

      +
        +
      • pageNumber: The page number
      • +
      +@param {callback=} onPrev (binding): Callback method to go to the previous page. +

      The callback returns:

      +
        +
      • pageNumber: The page number
      • +
      +@param {callback=} onGoToPage (binding): Callback method to go to a specific page. +

      The callback returns:

      +
        +
      • pageNumber: The page number
      • +
      +**/ + +(function() { + 'use strict'; + + function PaginationDirective() { + + function link(scope, el, attr, ctrl) { + + function activate() { + + scope.pagination = []; + + var i = 0; + + if (scope.totalPages <= 10) { + for (i = 0; i < scope.totalPages; i++) { + scope.pagination.push({ + val: (i + 1), + isActive: scope.pageNumber === (i + 1) + }); + } + } + else { + //if there is more than 10 pages, we need to do some fancy bits + + //get the max index to start + var maxIndex = scope.totalPages - 10; + //set the start, but it can't be below zero + var start = Math.max(scope.pageNumber - 5, 0); + //ensure that it's not too far either + start = Math.min(maxIndex, start); + + for (i = start; i < (10 + start) ; i++) { + scope.pagination.push({ + val: (i + 1), + isActive: scope.pageNumber === (i + 1) + }); + } + + //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing + if (start > 0) { + scope.pagination.unshift({ name: "First", val: 1, isActive: false }, {val: "...",isActive: false}); + } + + //same for the end + if (start < maxIndex) { + scope.pagination.push({ val: "...", isActive: false }, { name: "Last", val: scope.totalPages, isActive: false }); + } + } + + } + + scope.next = function() { + if (scope.onNext && scope.pageNumber < scope.totalPages) { + scope.pageNumber++; + scope.onNext(scope.pageNumber); + } + }; + + scope.prev = function(pageNumber) { + if (scope.onPrev && scope.pageNumber > 1) { + scope.pageNumber--; + scope.onPrev(scope.pageNumber); + } + }; + + scope.goToPage = function(pageNumber) { + if(scope.onGoToPage) { + scope.pageNumber = pageNumber + 1; + scope.onGoToPage(scope.pageNumber); + } + }; + + var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ + activate(); + }); + + scope.$on('$destroy', function(){ + unbindPageNumberWatcher(); + }); + + activate(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-pagination.html', + scope: { + pageNumber: "=", + totalPages: "=", + onNext: "=", + onPrev: "=", + onGoToPage: "=" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbPagination', PaginationDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js new file mode 100644 index 0000000000..cee705b8e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbstickybar.directive.js @@ -0,0 +1,154 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbStickyBar +@restrict A + +@description +Use this directive make an element sticky and follow the page when scrolling. + +

      Markup example

      +
      +    
      + +
      +
      + +
      +
      + +

      CSS example

      +
      +    .my-sticky-bar {
      +        padding: 15px 0;
      +        background: #000000;
      +        position: relative;
      +        top: 0;
      +    }
      +
      +    .my-sticky-bar.-umb-sticky-bar {
      +        top: 100px;
      +    }
      +
      + +@param {string} scrollableContainer Set the class (".element") or the id ("#element") of the scrollable container element. +**/ + +(function() { + 'use strict'; + + function StickyBarDirective($rootScope) { + + function link(scope, el, attr, ctrl) { + + var bar = $(el); + var scrollableContainer = null; + var clonedBar = null; + var cloneIsMade = false; + var barTop = bar.context.offsetTop; + + function activate() { + + if (attr.scrollableContainer) { + scrollableContainer = $(attr.scrollableContainer); + } else { + scrollableContainer = $(window); + } + + scrollableContainer.on('scroll.umbStickyBar', determineVisibility).trigger("scroll"); + $(window).on('resize.umbStickyBar', determineVisibility); + + scope.$on('$destroy', function() { + scrollableContainer.off('.umbStickyBar'); + $(window).off('.umbStickyBar'); + }); + + } + + function determineVisibility() { + + var scrollTop = scrollableContainer.scrollTop(); + + if (scrollTop > barTop) { + + if (!cloneIsMade) { + + createClone(); + + clonedBar.css({ + 'visibility': 'visible' + }); + + } else { + + calculateSize(); + + } + + } else { + + if (cloneIsMade) { + + //remove cloned element (switched places with original on creation) + bar.remove(); + bar = clonedBar; + clonedBar = null; + + bar.removeClass('-umb-sticky-bar'); + bar.css({ + position: 'relative', + 'width': 'auto', + 'height': 'auto', + 'z-index': 'auto', + 'visibility': 'visible' + }); + + cloneIsMade = false; + + } + + } + + } + + function calculateSize() { + clonedBar.css({ + width: bar.outerWidth(), + height: bar.height() + }); + } + + function createClone() { + //switch place with cloned element, to keep binding intact + clonedBar = bar; + bar = clonedBar.clone(); + clonedBar.after(bar); + clonedBar.addClass('-umb-sticky-bar'); + clonedBar.css({ + 'position': 'fixed', + 'z-index': 500, + 'visibility': 'hidden' + }); + + cloneIsMade = true; + calculateSize(); + + } + + activate(); + + } + + var directive = { + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbStickyBar', StickyBarDirective); + +})(); 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 new file mode 100644 index 0000000000..bf593397b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -0,0 +1,71 @@ +(function () { + 'use strict'; + + function TableDirective() { + + function link(scope, el, attr, ctrl) { + + scope.clickItem = function (item, $event) { + if (scope.onClick) { + scope.onClick(item); + $event.stopPropagation(); + } + }; + + scope.selectItem = function (item, $index, $event) { + if (scope.onSelect) { + scope.onSelect(item, $index, $event); + $event.stopPropagation(); + } + }; + + scope.selectAll = function ($event) { + if (scope.onSelectAll) { + scope.onSelectAll($event); + } + }; + + scope.isSelectedAll = function () { + if (scope.onSelectedAll && scope.items && scope.items.length > 0) { + return scope.onSelectedAll(); + } + }; + + scope.isSortDirection = function (col, direction) { + if (scope.onSortingDirection) { + return scope.onSortingDirection(col, direction); + } + }; + + scope.sort = function (field, allow, isSystem) { + if (scope.onSort) { + scope.onSort(field, allow, isSystem); + } + }; + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-table.html', + scope: { + items: '=', + itemProperties: '=', + allowSelectAll: '=', + onSelect: '=', + onClick: '=', + onSelectAll: '=', + onSelectedAll: '=', + onSortingDirection: '=', + onSort: '=' + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTable', TableDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js new file mode 100644 index 0000000000..d3bda41d43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtooltip.directive.js @@ -0,0 +1,164 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbTooltip +@restrict E +@scope + +@description +Use this directive to render a tooltip. + +

      Markup example

      +
      +    
      + +
      + Hover me +
      + + + // tooltip content here + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +
      +        function Controller() {
      +
      +            var vm = this;
      +            vm.tooltip = {
      +                show: false,
      +                event: null
      +            };
      +
      +            vm.mouseOver = mouseOver;
      +            vm.mouseLeave = mouseLeave;
      +
      +            function mouseOver($event) {
      +                vm.tooltip = {
      +                    show: true,
      +                    event: $event
      +                };
      +            }
      +
      +            function mouseLeave() {
      +                vm.tooltip = {
      +                    show: false,
      +                    event: null
      +                };
      +            }
      +
      +        }
      +
      +        angular.module("umbraco").controller("My.Controller", Controller);
      +
      +    })();
      +
      + +@param {string} event Set the $event from the target element to position the tooltip relative to the mouse cursor. +**/ + +(function() { + 'use strict'; + + function TooltipDirective($timeout) { + + function link(scope, el, attr, ctrl) { + + scope.tooltipStyles = {}; + scope.tooltipStyles.left = 0; + scope.tooltipStyles.top = 0; + + function activate() { + + $timeout(function() { + setTooltipPosition(scope.event); + }); + + } + + function setTooltipPosition(event) { + + var container = $("#contentwrapper"); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + + var elementHeight = null; + var elementWidth = null; + + var position = { + right: "inherit", + left: "inherit", + top: "inherit", + bottom: "inherit" + }; + + // element size + elementHeight = el.context.clientHeight; + elementWidth = el.context.clientWidth; + + position.left = event.pageX - (elementWidth / 2); + position.top = event.pageY; + + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = "inherit"; + } + + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = "inherit"; + } + + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = "inherit"; + } + + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = "inherit"; + } + + scope.tooltipStyles = position; + + el.css(position); + + } + + activate(); + + } + + var directive = { + restrict: 'E', + transclude: true, + replace: true, + templateUrl: 'views/components/umb-tooltip.html', + scope: { + event: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbTooltip', TooltipDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js new file mode 100644 index 0000000000..e0678c1065 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js @@ -0,0 +1,221 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:umbFileDropzone +* @restrict E +* @function +* @description +* Used by editors that require naming an entity. Shows a textbox/headline with a required validator within it's own form. +**/ + +/* +TODO +.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){ + + return{ + restrict: "A", + link: function(scope, element, attrs){ + + //load in the options model + + + } + } +}) +*/ + +angular.module("umbraco.directives") + +.directive('umbFileDropzone', function ($timeout, Upload, localizationService, umbRequestHelper) { + return { + + restrict: 'E', + replace: true, + + templateUrl: 'views/components/upload/umb-file-dropzone.html', + + scope: { + parentId: '@', + contentTypeAlias: '@', + propertyAlias: '@', + accept: '@', + maxFileSize: '@', + + compact: '@', + hideDropzone: '@', + + filesQueued: '=', + handleFile: '=', + filesUploaded: '=' + }, + + link: function(scope, element, attrs) { + + scope.queue = []; + scope.done = []; + scope.rejected = []; + scope.currentFile = undefined; + + function _filterFile(file) { + + var ignoreFileNames = ['Thumbs.db']; + var ignoreFileTypes = ['directory']; + + // ignore files with names from the list + // ignore files with types from the list + // ignore files which starts with "." + if(ignoreFileNames.indexOf(file.name) === -1 && + ignoreFileTypes.indexOf(file.type) === -1 && + file.name.indexOf(".") !== 0) { + return true; + } else { + return false; + } + + } + + function _filesQueued(files, event){ + + //Push into the queue + angular.forEach(files, function(file){ + + if(_filterFile(file) === true) { + + if(file.$error) { + scope.rejected.push(file); + } else { + scope.queue.push(file); + } + + } + + }); + + //when queue is done, kick the uploader + if(!scope.working){ + _processQueueItem(); + } + } + + + function _processQueueItem(){ + + if(scope.queue.length > 0){ + scope.currentFile = scope.queue.shift(); + _upload(scope.currentFile); + }else if(scope.done.length > 0){ + + if(scope.filesUploaded){ + //queue is empty, trigger the done action + scope.filesUploaded(scope.done); + } + + //auto-clear the done queue after 3 secs + var currentLength = scope.done.length; + $timeout(function(){ + scope.done.splice(0, currentLength); + }, 3000); + } + } + + function _upload(file) { + + scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : "umbracoFile"; + scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "Image"; + + Upload.upload({ + url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile"), + fields: { + 'currentFolder': scope.parentId, + 'contentTypeAlias': scope.contentTypeAlias, + 'propertyAlias': scope.propertyAlias, + 'path': file.path + }, + file: file + }).progress(function (evt) { + + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + + // set percentage property on file + file.uploadProgress = progressPercentage; + + // set uploading status on file + file.uploadStatus = "uploading"; + + }).success(function (data, status, headers, config) { + + if(data.notifications && data.notifications.length > 0) { + + // set error status on file + file.uploadStatus = "error"; + + // Throw message back to user with the cause of the error + file.serverErrorMessage = data.notifications[0].message; + + // Put the file in the rejected pool + scope.rejected.push(file); + + } else { + + // set done status on file + file.uploadStatus = "done"; + + // set date/time for when done - used for sorting + file.doneDate = new Date(); + + // Put the file in the done pool + scope.done.push(file); + + } + + scope.currentFile = undefined; + + //after processing, test if everthing is done + _processQueueItem(); + + }).error( function (evt, status, headers, config) { + + // set status done + file.uploadStatus = "error"; + + //if the service returns a detailed error + if (evt.InnerException) { + file.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + file.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + file.serverErrorMessage = evt.Message; + } + + // If file not found, server will return a 404 and display this message + if(status === 404 ) { + file.serverErrorMessage = "File not found"; + } + + //after processing, test if everthing is done + scope.rejected.push(file); + scope.currentFile = undefined; + + _processQueueItem(); + }); + } + + + scope.handleFiles = function(files, event){ + if(scope.filesQueued){ + scope.filesQueued(files, event); + } + + _filesQueued(files, event); + + }; + + } + + + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/umbfileupload.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfileupload.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbsinglefileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js similarity index 94% rename from src/Umbraco.Web.UI.Client/src/common/directives/editors/umbsinglefileupload.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js index b8123b9202..df4f68950e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbsinglefileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbsinglefileupload.directive.js @@ -1,6 +1,6 @@ /** * @ngdoc directive -* @name umbraco.directives.directive:umbFileUpload +* @name umbraco.directives.directive:umbSingleFileUpload * @function * @restrict A * @scope diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoResize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoResize.directive.js deleted file mode 100644 index dba9bf31f9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoResize.directive.js +++ /dev/null @@ -1,41 +0,0 @@ -angular.module("umbraco.directives") - .directive('umbAutoResize', function($timeout) { - return { - require: "^?umbTabs", - link: function(scope, element, attr, tabsCtrl) { - var domEl = element[0]; - var update = function(force) { - if (force === true) { - element.height(0); - } - - if (domEl.scrollHeight !== domEl.clientHeight) { - element.height(domEl.scrollHeight); - } - }; - var blur = function() { - update(true); - }; - - element.bind('keyup keydown keypress change', update); - element.bind('blur', blur); - - $timeout(function() { - update(true); - }, 200); - - - //listen for tab changes - if (tabsCtrl != null) { - tabsCtrl.onTabShown(function(args) { - update(); - }); - } - - scope.$on('$destroy', function () { - element.unbind('keyup keydown keypress change', update); - element.unbind('blur', blur); - }); - } - }; - }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbavatar.directive.js deleted file mode 100644 index 5979d07095..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbavatar.directive.js +++ /dev/null @@ -1,27 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbAvatar -* @restrict E -**/ -function avatarDirective() { - return { - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-avatar.html', - scope: { - name: '@', - email: '@', - hash: '@' - }, - link: function(scope, element, attr, ctrl) { - - scope.$watch("hash", function (val) { - //set the gravatar url - scope.gravatar = "https://www.gravatar.com/avatar/" + val + "?s=40"; - }); - - } - }; -} - -angular.module('umbraco.directives').directive("umbAvatar", avatarDirective); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbtabview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/html/umbtabview.directive.js deleted file mode 100644 index 7bb4f1cb18..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/html/umbtabview.directive.js +++ /dev/null @@ -1,14 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbTabView -* @restrict E -**/ -angular.module("umbraco.directives") -.directive('umbTabView', function($timeout, $log){ - return { - restrict: 'E', - replace: true, - transclude: 'true', - templateUrl: 'views/directives/umb-tab-view.html' - }; -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js deleted file mode 100644 index f7ca24303d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js +++ /dev/null @@ -1,171 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageFolder -* @restrict E -* @function -**/ -function umbImageFolder($rootScope, assetsService, $timeout, $log, umbRequestHelper, mediaResource, imageHelper, notificationsService) { - return { - restrict: 'E', - replace: true, - templateUrl: 'views/directives/imaging/umb-image-folder.html', - scope: { - options: '=', - nodeId: '@', - onUploadComplete: '=' - }, - link: function (scope, element, attrs) { - //NOTE: Blueimp handlers are documented here: https://github.com/blueimp/jQuery-File-Upload/wiki/Options - //NOTE: We are using a Blueimp version specifically ~9.4.0 because any higher than that and we get crazy errors with jquery, also note - // that the jquery UI version 1.10.3 is required for this blueimp version! if we go higher to 1.10.4 it breaks! seriously! - // It's do to with the widget framework in jquery ui changes which must have broken a whole lot of stuff. So don't change it for now. - - if (scope.onUploadComplete && !angular.isFunction(scope.onUploadComplete)) { - throw "onUploadComplete must be a function callback"; - } - - scope.uploading = false; - scope.files = []; - scope.progress = 0; - - var defaultOptions = { - url: umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostAddFile") + "?origin=blueimp", - //we'll start it manually to make sure the UI is all in order before processing - autoUpload: true, - disableImageResize: /Android(?!.*Chrome)|Opera/ - .test(window.navigator.userAgent), - previewMaxWidth: 150, - previewMaxHeight: 150, - previewCrop: true, - dropZone: element.find(".drop-zone"), - fileInput: element.find("input.uploader"), - formData: { - currentFolder: scope.nodeId - } - }; - - //merge options - scope.blueimpOptions = angular.extend(defaultOptions, scope.options); - - function loadChildren(id) { - mediaResource.getChildren(id) - .then(function(data) { - scope.images = data.items; - }); - } - - function checkComplete(e, data) { - scope.$apply(function () { - //remove the amount of files complete - //NOTE: function is here instead of in the loop otherwise jshint blows up - function findFile(file) { return file === data.files[i]; } - for (var i = 0; i < data.files.length; i++) { - var found = _.find(scope.files, findFile); - found.completed = true; - } - - //Show notifications!!!! - if (data.result && data.result.notifications && angular.isArray(data.result.notifications)) { - for (var n = 0; n < data.result.notifications.length; n++) { - notificationsService.showNotification(data.result.notifications[n]); - } - } - - //when none are left resync everything - var remaining = _.filter(scope.files, function (file) { return file.completed !== true; }); - if (remaining.length === 0) { - - scope.progress = 100; - - //just the ui transition isn't too abrupt, just wait a little here - $timeout(function () { - scope.progress = 0; - scope.files = []; - scope.uploading = false; - - loadChildren(scope.nodeId); - - //call the callback - scope.onUploadComplete.apply(this, [data]); - - - }, 200); - - - } - }); - } - - //when one is finished - scope.$on('fileuploaddone', function(e, data) { - checkComplete(e, data); - }); - - //This handler gives us access to the file 'preview', this is the only handler that makes this available for whatever reason - // so we'll use this to also perform the adding of files to our collection - scope.$on('fileuploadprocessalways', function(e, data) { - scope.$apply(function() { - scope.uploading = true; - scope.files.push(data.files[data.index]); - }); - }); - - //This is a bit of a hack to check for server errors, currently if there's a non - //known server error we will tell them to check the logs, otherwise we'll specifically - //check for the file size error which can only be done with dodgy string checking - scope.$on('fileuploadfail', function (e, data) { - if (data.jqXHR.status === 500 && data.jqXHR.responseText.indexOf("Maximum request length exceeded") >= 0) { - notificationsService.error(data.errorThrown, "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed"); - - } - else { - notificationsService.error(data.errorThrown, data.jqXHR.statusText); - } - - checkComplete(e, data); - }); - - //This executes prior to the whole processing which we can use to get the UI going faster, - //this also gives us the start callback to invoke to kick of the whole thing - scope.$on('fileuploadadd', function(e, data) { - scope.$apply(function() { - scope.uploading = true; - }); - }); - - // All these sit-ups are to add dropzone area and make sure it gets removed if dragging is aborted! - scope.$on('fileuploaddragover', function(e, data) { - if (!scope.dragClearTimeout) { - scope.$apply(function() { - scope.dropping = true; - }); - } - else { - $timeout.cancel(scope.dragClearTimeout); - } - scope.dragClearTimeout = $timeout(function() { - scope.dropping = null; - scope.dragClearTimeout = null; - }, 300); - }); - - //init load - loadChildren(scope.nodeId); - - } - }; -} - -angular.module("umbraco.directives") - .directive("umbUploadPreview", function($parse) { - return { - link: function(scope, element, attr, ctrl) { - var fn = $parse(attr.umbUploadPreview), - file = fn(scope); - if (file.preview) { - element.append(file.preview); - } - } - }; - }) - .directive('umbImageFolder', umbImageFolder); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimageupload.js b/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimageupload.js deleted file mode 100644 index be7336cbc0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimageupload.js +++ /dev/null @@ -1,39 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageFileUpload -* @restrict E -* @function -* @description -* This is a wrapper around the blueimp angular file-upload directive so that we can expose a proper API to other directives, the blueimp -* directive isn't actually made very well and only exposes an API/events on the $scope so we can't do things like require: "^fileUpload" and use -* it's instance. -**/ -function umbImageUpload($compile) { - return { - restrict: 'A', - scope: true, - link: function (scope, element, attrs) { - //set inner scope variable to assign to file-upload directive in the template - scope.innerOptions = scope.$eval(attrs.umbImageUpload); - - //compile an inner blueimp file-upload with our scope - - var x = angular.element('
      '); - element.append(x); - $compile(x)(scope); - }, - - //Define a controller for this directive to expose APIs to other directives - controller: function ($scope, $element, $attrs) { - - - //create a method to allow binding to a blueimp event (which is based on it's directives scope) - this.bindEvent = function (e, callback) { - $scope.$on(e, callback); - }; - - } - }; -} - -angular.module("umbraco.directives").directive('umbImageUpload', umbImageUpload); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimageuploadprogress.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimageuploadprogress.directive.js deleted file mode 100644 index 6101ee4a01..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimageuploadprogress.directive.js +++ /dev/null @@ -1,23 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:umbImageUploadProgress -* @restrict E -* @function -**/ -function umbImageUploadProgress($rootScope, assetsService, $timeout, $log, umbRequestHelper, mediaResource, imageHelper) { - return { - require: '^umbImageUpload', - restrict: 'E', - replace: true, - template: '
      ', - - link: function (scope, element, attrs, umbImgUploadCtrl) { - - umbImgUploadCtrl.bindEvent('fileuploadprogressall', function (e, data) { - scope.progress = parseInt(data.loaded / data.total * 100, 10); - }); - } - }; -} - -angular.module("umbraco.directives").directive('umbImageUploadProgress', umbImageUploadProgress); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbeditor.directive.js deleted file mode 100644 index 7446ef8f16..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbeditor.directive.js +++ /dev/null @@ -1,33 +0,0 @@ -/** -* @ngdoc directive -* @function -* @name umbraco.directives.directive:umbEditor -* @requires formController -* @restrict E -**/ -angular.module("umbraco.directives") - .directive('umbEditor', function (umbPropEditorHelper) { - return { - scope: { - model: "=", - isPreValue: "@" - }, - require: "^form", - restrict: 'E', - replace: true, - templateUrl: 'views/directives/umb-editor.html', - link: function (scope, element, attrs, ctrl) { - - //we need to copy the form controller val to our isolated scope so that - //it get's carried down to the child scopes of this! - //we'll also maintain the current form name. - scope[ctrl.$name] = ctrl; - - if(!scope.model.alias){ - scope.model.alias = Math.random().toString(36).slice(2); - } - - scope.propertyEditorView = umbPropEditorHelper.getViewPath(scope.model.view, scope.isPreValue); - } - }; - }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js deleted file mode 100644 index 365c212f42..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @ngdoc directive - * @name umbraco.directives.directive:umbNotifications - */ -function notificationDirective(notificationsService) { - return { - restrict: "E", // restrict to an element - replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-notifications.html', - link: function (scope, element, attr, ctrl) { - - //subscribes to notifications in the notification service - scope.notifications = notificationsService.current; - scope.$watch('notificationsService.current', function (newVal, oldVal, scope) { - if (newVal) { - scope.notifications = newVal; - } - }); - - } - }; -} - -angular.module('umbraco.directives').directive("umbNotifications", notificationDirective); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util.directive.js deleted file mode 100644 index ebc4bcea7e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util.directive.js +++ /dev/null @@ -1,56 +0,0 @@ -/** -* @description Utillity directives for key and field events -**/ -angular.module('umbraco.directives') - -.directive('onKeyup', function () { - return function (scope, elm, attrs) { - elm.bind("keyup", function () { - scope.$apply(attrs.onKeyup); - }); - }; -}) - -.directive('onKeydown', function () { - return { - link: function (scope, elm, attrs) { - scope.$apply(attrs.onKeydown); - } - }; -}) - -.directive('onBlur', function () { - return function (scope, elm, attrs) { - elm.bind("blur", function () { - scope.$apply(attrs.onBlur); - }); - }; -}) - -.directive('onFocus', function () { - return function (scope, elm, attrs) { - elm.bind("focus", function () { - scope.$apply(attrs.onFocus); - }); - }; -}) - -.directive('onRightClick',function(){ - - document.oncontextmenu = function (e) { - if(e.target.hasAttribute('on-right-click')) { - e.preventDefault(); - e.stopPropagation(); - return false; - } - }; - - return function(scope,el,attrs){ - el.bind('contextmenu',function(e){ - e.preventDefault(); - e.stopPropagation(); - scope.$apply(attrs.onRightClick); - return false; - }); - }; -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js deleted file mode 100644 index 11fff9639f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @ngdoc directive - * @name umbraco.directives.directive:autoScale - * @element div - * @function - * - * @description - * Resize div's automatically to fit to the bottom of the screen, as an optional parameter an y-axis offset can be set - * So if you only want to scale the div to 70 pixels from the bottom you pass "70" - * - * @example - - -
      -
      -
      - */ -angular.module("umbraco.directives") - .directive('autoScale', function ($window) { - return function (scope, el, attrs) { - - var totalOffset = 0; - var offsety = parseInt(attrs.autoScale, 10); - var window = angular.element($window); - if (offsety !== undefined){ - totalOffset += offsety; - } - - setTimeout(function () { - el.height(window.height() - (el.offset().top + totalOffset)); - }, 500); - - window.bind("resize", function () { - el.height(window.height() - (el.offset().top + totalOffset)); - }); - - }; - }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js deleted file mode 100644 index 28a90f9041..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/delayedMouseLeave.directive.js +++ /dev/null @@ -1,36 +0,0 @@ -angular.module("umbraco.directives") - .directive('delayedMouseleave', function ($timeout, $parse) { - return { - restrict: 'A', - link: function (scope, element, attrs, ctrl) { - var active = false; - var fn = $parse(attrs.delayedMouseleave); - - function mouseLeave(event) { - var callback = function () { - fn(scope, { $event: event }); - }; - - active = false; - $timeout(function () { - if (active === false) { - scope.$apply(callback); - } - }, 650); - } - - function mouseEnter(event, args){ - active = true; - } - - element.on("mouseleave", mouseLeave); - element.on("mouseenter", mouseEnter); - - //unbind!! - scope.$on('$destroy', function () { - element.off("mouseleave", mouseLeave); - element.off("mouseenter", mouseEnter); - }); - } - }; - }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/hotkey.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/hotkey.directive.js deleted file mode 100644 index 2ed1c87220..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/hotkey.directive.js +++ /dev/null @@ -1,28 +0,0 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:headline -**/ -angular.module("umbraco.directives") - .directive('hotkey', function ($window, keyboardService, $log) { - - return function (scope, el, attrs) { - - //support data binding - - var keyCombo = scope.$eval(attrs["hotkey"]); - if (!keyCombo) { - keyCombo = attrs["hotkey"]; - } - - keyboardService.bind(keyCombo, function() { - var element = $(el); - - if(element.is("a,button,input[type='button'],input[type='submit']") && !element.is(':disabled') ){ - element.click(); - }else{ - element.focus(); - } - }); - - }; - }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js new file mode 100644 index 0000000000..7c96e0e050 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js @@ -0,0 +1,62 @@ +/** + * Konami Code directive for AngularJS + * @version v0.0.1 + * @license MIT License, http://www.opensource.org/licenses/MIT + */ + +angular.module('umbraco.directives') + .directive('konamiCode', ['$document', function ($document) { + var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; + + return { + restrict: 'A', + link: function (scope, element, attr) { + + if (!attr.konamiCode) { + throw ('Konami directive must receive an expression as value.'); + } + + // Let user define a custom code. + var konamiKeys = attr.konamiKeys || konamiKeysDefault; + var keyIndex = 0; + + /** + * Fired when konami code is type. + */ + function activated() { + if ('konamiOnce' in attr) { + stopListening(); + } + // Execute expression. + scope.$eval(attr.konamiCode); + } + + /** + * Handle keydown events. + */ + function keydown(e) { + if (e.keyCode === konamiKeys[keyIndex++]) { + if (keyIndex === konamiKeys.length) { + keyIndex = 0; + activated(); + } + } else { + keyIndex = 0; + } + } + + /** + * Stop to listen typing. + */ + function stopListening() { + $document.off('keydown', keydown); + } + + // Start listening to key typing. + $document.on('keydown', keydown); + + // Stop listening when scope is destroyed. + scope.$on('$destroy', stopListening); + } + }; + }]); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/localize.directive.js deleted file mode 100644 index 113f24ad5f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/localize.directive.js +++ /dev/null @@ -1,37 +0,0 @@ -angular.module("umbraco.directives") -.directive('localize', function ($log, localizationService) { - return { - restrict: 'E', - scope:{ - key: '@' - }, - replace: true, - link: function (scope, element, attrs) { - var key = scope.key; - localizationService.localize(key).then(function(value){ - element.html(value); - }); - } - }; -}) -.directive('localize', function ($log, localizationService) { - return { - restrict: 'A', - link: function (scope, element, attrs) { - var keys = attrs.localize.split(','); - - angular.forEach(keys, function(value, key){ - var attr = element.attr(value); - if(attr){ - if(attr[0] === '@'){ - var t = localizationService.tokenize(attr.substring(1), scope); - localizationService.localize(t.key, t.tokens).then(function(val){ - element.attr(value, val); - }); - } - } - }); - - } - }; -}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/umbsetdirtyonchange.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/umbsetdirtyonchange.directive.js new file mode 100644 index 0000000000..37a773ae97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/umbsetdirtyonchange.directive.js @@ -0,0 +1,29 @@ +(function() { + 'use strict'; + + function SetDirtyOnChange() { + + function link(scope, el, attr, ctrl) { + + var initValue = attr.umbSetDirtyOnChange; + + attr.$observe("umbSetDirtyOnChange", function (newValue) { + if(newValue !== initValue) { + ctrl.$setDirty(); + } + }); + + } + + var directive = { + require: "^form", + restrict: 'A', + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbSetDirtyOnChange', SetDirtyOnChange); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js index a402065708..dac010a97f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js @@ -57,7 +57,7 @@ angular.module('umbraco.directives.validation') } }; validators[key] = validateFn; - ctrl.$formatters.push(validateFn); + ctrl.$parsers.push(validateFn); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js index fdcf768947..9182441f8b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js @@ -1,30 +1,28 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:valHighlight -* @restrict A -* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second -**/ -function valHighlight($timeout) { - return { - restrict: "A", - link: function (scope, element, attrs, ctrl) { - - scope.$watch(function() { - return scope.$eval(attrs.valHighlight); - }, function(newVal, oldVal) { - if (newVal === true) { - element.addClass("highlight-error"); - $timeout(function () { - //set the bound scope property to false - scope[attrs.valHighlight] = false; - }, 1000); - } - else { - element.removeClass("highlight-error"); - } - }); - - } - }; -} -angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); \ No newline at end of file +/** +* @ngdoc directive +* @name umbraco.directives.directive:valHighlight +* @restrict A +* @description Used on input fields when you want to signal that they are in error, this will highlight the item for 1 second +**/ +function valHighlight($timeout) { + return { + restrict: "A", + link: function (scope, element, attrs, ctrl) { + + attrs.$observe("valHighlight", function (newVal) { + if (newVal === "true") { + element.addClass("highlight-error"); + $timeout(function () { + //set the bound scope property to false + scope[attrs.valHighlight] = false; + }, 1000); + } + else { + element.removeClass("highlight-error"); + } + }); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valHighlight", valHighlight); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js index 31595273de..1a36dcc24f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js @@ -1,24 +1,24 @@ -angular.module('umbraco.directives.validation') - .directive('valCompare',function () { - return { - require: "ngModel", - link: function (scope, elem, attrs, ctrl) { - - //TODO: Pretty sure this should be done using a requires ^form in the directive declaration - var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; - - ctrl.$parsers.push(function(value) { - if(value === otherInput.$viewValue) { - ctrl.$setValidity("valCompare", true); - return value; - } - ctrl.$setValidity("valCompare", false); - }); - - otherInput.$parsers.push(function(value) { - ctrl.$setValidity("valCompare", value === ctrl.$viewValue); - return value; - }); - } - }; +angular.module('umbraco.directives.validation') + .directive('valCompare',function () { + return { + require: "ngModel", + link: function (scope, elem, attrs, ctrl) { + + //TODO: Pretty sure this should be done using a requires ^form in the directive declaration + var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; + + ctrl.$parsers.push(function(value) { + if(value === otherInput.$viewValue) { + ctrl.$setValidity("valCompare", true); + return value; + } + ctrl.$setValidity("valCompare", false); + }); + + otherInput.$parsers.push(function(value) { + ctrl.$setValidity("valCompare", value === ctrl.$viewValue); + return value; + }); + } + }; }); \ No newline at end of file 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 88ffd6f0fa..8574d01f5a 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 @@ -29,7 +29,16 @@ function valEmail(valEmailExpression) { } }; - ctrl.$formatters.push(patternValidator); + //if there is an attribute: type="email" then we need to remove those formatters and parsers + if (attrs.type === "email") { + //we need to remove the existing parsers = the default angular one which is created by + // type="email", but this has a regex issue, so we'll remove that and add our custom one + ctrl.$parsers.pop(); + //we also need to remove the existing formatter - the default angular one will not render + // what it thinks is an invalid email address, so it will just be blank + ctrl.$formatters.pop(); + } + ctrl.$parsers.push(patternValidator); } }; @@ -37,7 +46,8 @@ function valEmail(valEmailExpression) { angular.module('umbraco.directives.validation') .directive("valEmail", valEmail) - .factory('valEmailExpression', function() { + .factory('valEmailExpression', function () { + //NOTE: This is the fixed regex which is part of the newer angular 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 }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 634f6eb4ec..9a00d5718c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -1,112 +1,133 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:valFormManager -* @restrict A -* @require formController -* @description Used to broadcast an event to all elements inside this one to notify that form validation has -* changed. If we don't use this that means you have to put a watch for each directive on a form's validation -* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form -* because just watching $valid or $invalid doesn't acurrately trigger form validation changing. -* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets -* us css target elements to be displayed when the form is submitting/submitted. -* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will -* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. -**/ -function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { - return { - require: "form", - restrict: "A", - link: function (scope, element, attr, formCtrl) { - - scope.$watch(function () { - return formCtrl.$error; - }, function (e) { - scope.$broadcast("valStatusChanged", { form: formCtrl }); - - //find all invalid elements' .control-group's and apply the error class - var inError = element.find(".control-group .ng-invalid").closest(".control-group"); - inError.addClass("error"); - - //find all control group's that have no error and ensure the class is removed - var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); - noInError.removeClass("error"); - - }, true); - - var className = attr.valShowValidation ? attr.valShowValidation : "show-validation"; - var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting"; - var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted"; - - //This tracks if the user is currently saving a new item, we use this to determine - // if we should display the warning dialog that they are leaving the page - if a new item - // is being saved we never want to display that dialog, this will also cause problems when there - // are server side validation issues. - var isSavingNewItem = false; - - //we should show validation if there are any msgs in the server validation collection - if (serverValidationManager.items.length > 0) { - element.addClass(className); - } - - var unsubscribe = []; - - //listen for the forms saving event - unsubscribe.push(scope.$on(savingEventName, function(ev, args) { - element.addClass(className); - - //set the flag so we can check to see if we should display the error. - isSavingNewItem = $routeParams.create; - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on(savedEvent, function(ev, args) { - //remove validation class - element.removeClass(className); - - //clear form state as at this point we retrieve new data from the server - //and all validation will have cleared at this point - formCtrl.$setPristine(); - })); - - //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but - // the form has pending changes - var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { - if (!formCtrl.$dirty || isSavingNewItem) { - return; - } - - var path = nextLocation.split("#")[1]; - if (path) { - if (path.indexOf("%253") || path.indexOf("%252")) { - path = decodeURIComponent(path); - } - - if (!notificationsService.hasView()) { - var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } }; - notificationsService.add(msg); - } - - //prevent the route! - event.preventDefault(); - - //raise an event - eventsService.emit("valFormManager.pendingChanges", true); - } - - }); - unsubscribe.push(locationEvent); - - //Ensure to remove the event handler when this instance is destroyted - scope.$on('$destroy', function() { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - $timeout(function(){ - formCtrl.$setPristine(); - }, 1000); - } - }; -} +/** +* @ngdoc directive +* @name umbraco.directives.directive:valFormManager +* @restrict A +* @require formController +* @description Used to broadcast an event to all elements inside this one to notify that form validation has +* changed. If we don't use this that means you have to put a watch for each directive on a form's validation +* changing which would result in much higher processing. We need to actually watch the whole $error collection of a form +* because just watching $valid or $invalid doesn't acurrately trigger form validation changing. +* This also sets the show-validation (or a custom) css class on the element when the form is invalid - this lets +* us css target elements to be displayed when the form is submitting/submitted. +* Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will +* be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. +**/ +function valFormManager(serverValidationManager, $rootScope, $log, $timeout, notificationsService, eventsService, $routeParams) { + return { + require: "form", + restrict: "A", + controller: function($scope) { + //This exposes an API for direct use with this directive + + var unsubscribe = []; + var self = this; + + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { + cb.apply(self, [evt, args]); + })); + }; + + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + }, + link: function (scope, element, attr, formCtrl) { + + scope.$watch(function () { + return formCtrl.$error; + }, function (e) { + scope.$broadcast("valStatusChanged", { form: formCtrl }); + + //find all invalid elements' .control-group's and apply the error class + var inError = element.find(".control-group .ng-invalid").closest(".control-group"); + inError.addClass("error"); + + //find all control group's that have no error and ensure the class is removed + var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); + noInError.removeClass("error"); + + }, true); + + var className = attr.valShowValidation ? attr.valShowValidation : "show-validation"; + var savingEventName = attr.savingEvent ? attr.savingEvent : "formSubmitting"; + var savedEvent = attr.savedEvent ? attr.savingEvent : "formSubmitted"; + + //This tracks if the user is currently saving a new item, we use this to determine + // if we should display the warning dialog that they are leaving the page - if a new item + // is being saved we never want to display that dialog, this will also cause problems when there + // are server side validation issues. + var isSavingNewItem = false; + + //we should show validation if there are any msgs in the server validation collection + if (serverValidationManager.items.length > 0) { + element.addClass(className); + } + + var unsubscribe = []; + + //listen for the forms saving event + unsubscribe.push(scope.$on(savingEventName, function(ev, args) { + element.addClass(className); + + //set the flag so we can check to see if we should display the error. + isSavingNewItem = $routeParams.create; + })); + + //listen for the forms saved event + unsubscribe.push(scope.$on(savedEvent, function(ev, args) { + //remove validation class + element.removeClass(className); + + //clear form state as at this point we retrieve new data from the server + //and all validation will have cleared at this point + formCtrl.$setPristine(); + })); + + //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but + // the form has pending changes + var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { + if (!formCtrl.$dirty || isSavingNewItem) { + return; + } + + var path = nextLocation.split("#")[1]; + if (path) { + if (path.indexOf("%253") || path.indexOf("%252")) { + path = decodeURIComponent(path); + } + + if (!notificationsService.hasView()) { + var msg = { view: "confirmroutechange", args: { path: path, listener: locationEvent } }; + notificationsService.add(msg); + } + + //prevent the route! + event.preventDefault(); + + //raise an event + eventsService.emit("valFormManager.pendingChanges", true); + } + + }); + unsubscribe.push(locationEvent); + + //Ensure to remove the event handler when this instance is destroyted + scope.$on('$destroy', function() { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + + $timeout(function(){ + formCtrl.$setPristine(); + }, 1000); + } + }; +} angular.module('umbraco.directives.validation').directive("valFormManager", valFormManager); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index eba308d830..be5da51702 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -1,196 +1,196 @@ -/** -* @ngdoc directive -* @name umbraco.directives.directive:valPropertyMsg -* @restrict A -* @element textarea -* @requires formController -* @description This directive is used to control the display of the property level validation message. -* We will listen for server side validation changes -* and when an error is detected for this property we'll show the error message. -* In order for this directive to work, the valStatusChanged directive must be placed on the containing form. -**/ -function valPropertyMsg(serverValidationManager) { - - return { - scope: { - property: "=" - }, - require: "^form", //require that this directive is contained within an ngForm - replace: true, //replace the element with the template - restrict: "E", //restrict to element - template: "
      {{errorMsg}}
      ", - - /** - Our directive requries a reference to a form controller - which gets passed in to this parameter - */ - link: function (scope, element, attrs, formCtrl) { - - var watcher = null; - - // Gets the error message to display - function getErrorMsg() { - //this can be null if no property was assigned - if (scope.property) { - //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.property.alias, ""); - //if there's an error message use it - if (err && err.errorMsg) { - return err.errorMsg; - } - else { - return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors"; - } - - } - return "Property has errors"; - } - - // We need to subscribe to any changes to our model (based on user input) - // This is required because when we have a server error we actually invalidate - // the form which means it cannot be resubmitted. - // So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - function startWatch() { - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch("property.value", function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { - return; - } - - var errCount = 0; - for (var e in formCtrl.$error) { - if (angular.isArray(formCtrl.$error[e])) { - errCount++; - } - } - - //we are explicitly checking for valServer errors here, since we shouldn't auto clear - // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg - // is the only one, then we'll clear. - - if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - } - }, true); - } - } - - //clear the watch when the property validator is valid again - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } - } - - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - var hasError = false; - - //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; - - var unsubscribe = []; - - //listen for form error changes - unsubscribe.push(scope.$on("valStatusChanged", function(evt, args) { - if (args.form.$invalid) { - - //first we need to check if the valPropertyMsg validity is invalid - if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { - //since we already have an error we'll just return since this means we've already set the - // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe - return; - } - else if (element.closest(".umb-control-group").find(".ng-invalid").length > 0) { - //check if it's one of the properties that is invalid in the current content property - hasError = true; - //update the validation message if we don't already have one assigned. - if (showValidation && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - } - } - else { - hasError = false; - scope.errorMsg = ""; - } - } - else { - hasError = false; - scope.errorMsg = ""; - } - }, true)); - - //listen for the forms saving event - unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { - showValidation = true; - if (hasError && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - } - else if (!hasError) { - scope.errorMsg = ""; - stopWatch(); - } - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { - showValidation = false; - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - })); - - //listen for server validation changes - // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for - // validation changes to fields in the property this is because some server side validators may not - // return the field name for which the error belongs too, just the property for which it belongs. - // It's important to note that we need to subscribe to server validation changes here because we always must - // indicate that a content property is invalid at the property level since developers may not actually implement - // the correct field validation in their property editors. - - if (scope.property) { //this can be null if no property was assigned - serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) { - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); - startWatch(); - } - else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - stopWatch(); - serverValidationManager.unsubscribe(scope.property.alias, ""); - }); - } - - //when the scope is disposed we need to unsubscribe - scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - } - - - }; -} +/** +* @ngdoc directive +* @name umbraco.directives.directive:valPropertyMsg +* @restrict A +* @element textarea +* @requires formController +* @description This directive is used to control the display of the property level validation message. +* We will listen for server side validation changes +* and when an error is detected for this property we'll show the error message. +* In order for this directive to work, the valStatusChanged directive must be placed on the containing form. +**/ +function valPropertyMsg(serverValidationManager) { + + return { + scope: { + property: "=" + }, + require: "^form", //require that this directive is contained within an ngForm + replace: true, //replace the element with the template + restrict: "E", //restrict to element + template: "
      {{errorMsg}}
      ", + + /** + Our directive requries a reference to a form controller + which gets passed in to this parameter + */ + link: function (scope, element, attrs, formCtrl) { + + var watcher = null; + + // Gets the error message to display + function getErrorMsg() { + //this can be null if no property was assigned + if (scope.property) { + //first try to get the error msg from the server collection + var err = serverValidationManager.getPropertyError(scope.property.alias, ""); + //if there's an error message use it + if (err && err.errorMsg) { + return err.errorMsg; + } + else { + return scope.property.propertyErrorMessage ? scope.property.propertyErrorMessage : "Property has errors"; + } + + } + return "Property has errors"; + } + + // We need to subscribe to any changes to our model (based on user input) + // This is required because when we have a server error we actually invalidate + // the form which means it cannot be resubmitted. + // So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch("property.value", function (newValue, oldValue) { + + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + + var errCount = 0; + for (var e in formCtrl.$error) { + if (angular.isArray(formCtrl.$error[e])) { + errCount++; + } + } + + //we are explicitly checking for valServer errors here, since we shouldn't auto clear + // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg + // is the only one, then we'll clear. + + if ((errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { + scope.errorMsg = ""; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + }, true); + } + } + + //clear the watch when the property validator is valid again + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + var hasError = false; + + //create properties on our custom scope so we can use it in our template + scope.errorMsg = ""; + + var unsubscribe = []; + + //listen for form error changes + unsubscribe.push(scope.$on("valStatusChanged", function(evt, args) { + if (args.form.$invalid) { + + //first we need to check if the valPropertyMsg validity is invalid + if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { + //since we already have an error we'll just return since this means we've already set the + // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe + return; + } + else if (element.closest(".umb-control-group").find(".ng-invalid").length > 0) { + //check if it's one of the properties that is invalid in the current content property + hasError = true; + //update the validation message if we don't already have one assigned. + if (showValidation && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + } + } + else { + hasError = false; + scope.errorMsg = ""; + } + } + else { + hasError = false; + scope.errorMsg = ""; + } + }, true)); + + //listen for the forms saving event + unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { + showValidation = true; + if (hasError && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + } + else if (!hasError) { + scope.errorMsg = ""; + stopWatch(); + } + })); + + //listen for the forms saved event + unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { + showValidation = false; + scope.errorMsg = ""; + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + })); + + //listen for server validation changes + // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for + // validation changes to fields in the property this is because some server side validators may not + // return the field name for which the error belongs too, just the property for which it belongs. + // It's important to note that we need to subscribe to server validation changes here because we always must + // indicate that a content property is invalid at the property level since developers may not actually implement + // the correct field validation in their property editors. + + if (scope.property) { //this can be null if no property was assigned + serverValidationManager.subscribe(scope.property.alias, "", function (isValid, propertyErrors, allErrors) { + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors[0].errorMsg; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + } + else { + scope.errorMsg = ""; + //flag that the current validator is valid + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + stopWatch(); + serverValidationManager.unsubscribe(scope.property.alias, ""); + }); + } + + //when the scope is disposed we need to unsubscribe + scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + + + }; +} angular.module('umbraco.directives.validation').directive("valPropertyMsg", valPropertyMsg); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js index 77652d7f69..53a1ea67b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js @@ -59,9 +59,6 @@ function valPropertyValidator(serverValidationManager) { } }; - // Formatters are invoked when the model is modified in the code. - modelCtrl.$formatters.push(validate); - // Parsers are called as soon as the value in the form input is modified modelCtrl.$parsers.push(validate); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js index 651c0a54c7..6406583e77 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js @@ -1,64 +1,78 @@ -/** - * @ngdoc directive - * @name umbraco.directives.directive:valRegex - * @restrict A - * @description A custom directive to allow for matching a value against a regex string. - * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string - **/ -function valRegex() { - - return { - require: 'ngModel', - restrict: "A", - link: function (scope, elm, attrs, ctrl) { - - var flags = ""; - if (attrs.valRegexFlags) { - try { - flags = scope.$eval(attrs.valRegexFlags); - if (!flags) { - flags = attrs.valRegexFlags; - } - } - catch (e) { - flags = attrs.valRegexFlags; - } - } - var regex; - try { - var resolved = scope.$eval(attrs.valRegex); - if (resolved) { - regex = new RegExp(resolved, flags); - } - else { - regex = new RegExp(attrs.valRegex, flags); - } - } - catch(e) { - regex = new RegExp(attrs.valRegex, flags); - } - - var patternValidator = function (viewValue) { - //NOTE: we don't validate on empty values, use required validator for that - if (!viewValue || regex.test(viewValue)) { - // it is valid - ctrl.$setValidity('valRegex', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; - } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valRegex', false); - //assign a message to the validator - ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; - return undefined; - } - }; - - ctrl.$formatters.push(patternValidator); - ctrl.$parsers.push(patternValidator); - } - }; -} -angular.module('umbraco.directives.validation').directive("valRegex", valRegex); \ No newline at end of file +/** + * @ngdoc directive + * @name umbraco.directives.directive:valRegex + * @restrict A + * @description A custom directive to allow for matching a value against a regex string. + * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string + **/ +function valRegex() { + + return { + require: 'ngModel', + restrict: "A", + link: function (scope, elm, attrs, ctrl) { + + var flags = ""; + var regex; + var eventBindings = []; + + attrs.$observe("valRegexFlags", function (newVal) { + if (newVal) { + flags = newVal; + } + }); + + attrs.$observe("valRegex", function (newVal) { + if (newVal) { + try { + var resolved = newVal; + if (resolved) { + regex = new RegExp(resolved, flags); + } + else { + regex = new RegExp(attrs.valRegex, flags); + } + } + catch (e) { + regex = new RegExp(attrs.valRegex, flags); + } + } + }); + + eventBindings.push(scope.$watch('ngModel', function(newValue, oldValue){ + if(newValue && newValue !== oldValue) { + patternValidator(newValue); + } + })); + + var patternValidator = function (viewValue) { + if (regex) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || regex.test(viewValue.toString())) { + // it is valid + ctrl.$setValidity('valRegex', true); + //assign a message to the validator + ctrl.errorMsg = ""; + return viewValue; + } + else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valRegex', false); + //assign a message to the validator + ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; + return undefined; + } + } + }; + + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valRegex", valRegex); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valrequirecomponent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valrequirecomponent.directive.js new file mode 100644 index 0000000000..565136608f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valrequirecomponent.directive.js @@ -0,0 +1,38 @@ +(function() { + 'use strict'; + + function ValRequireComponentDirective() { + + function link(scope, el, attr, ngModel) { + + var unbindModelWatcher = scope.$watch(function () { + return ngModel.$modelValue; + }, function(newValue) { + + if(newValue === undefined || newValue === null || newValue === "") { + ngModel.$setValidity("valRequiredComponent", false); + } else { + ngModel.$setValidity("valRequiredComponent", true); + } + + }); + + // clean up + scope.$on('$destroy', function(){ + unbindModelWatcher(); + }); + + } + + var directive = { + require: 'ngModel', + restrict: "A", + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('valRequireComponent', ValRequireComponentDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 6225485073..1432a713c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -1,94 +1,94 @@ -/** - * @ngdoc directive - * @name umbraco.directives.directive:valServer - * @restrict A - * @description This directive is used to associate a content property with a server-side validation response - * so that the validators in angular are updated based on server-side feedback. - **/ -function valServer(serverValidationManager) { - return { - require: ['ngModel', '?^umbProperty'], - restrict: "A", - link: function (scope, element, attr, ctrls) { - - var modelCtrl = ctrls[0]; - var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; - if (!umbPropCtrl) { - //we cannot proceed, this validator will be disabled - return; - } - - var watcher = null; - - //Need to watch the value model for it to change, previously we had subscribed to - //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that - // doesn't specifically have a 2 way ng binding. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - function startWatch() { - //if there's not already a watch - if (!watcher) { - watcher = scope.$watch(function () { - return modelCtrl.$modelValue; - }, function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { - return; - } - - if (modelCtrl.$invalid) { - modelCtrl.$setValidity('valServer', true); - stopWatch(); - } - }, true); - } - } - - function stopWatch() { - if (watcher) { - watcher(); - watcher = null; - } - } - - var currentProperty = umbPropCtrl.property; - - //default to 'value' if nothing is set - var fieldName = "value"; - if (attr.valServer) { - fieldName = scope.$eval(attr.valServer); - if (!fieldName) { - //eval returned nothing so just use the string - fieldName = attr.valServer; - } - } - - //subscribe to the server validation changes - serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { - if (!isValid) { - modelCtrl.$setValidity('valServer', false); - //assign an error msg property to the current validator - modelCtrl.errorMsg = propertyErrors[0].errorMsg; - startWatch(); - } - else { - modelCtrl.$setValidity('valServer', true); - //reset the error message - modelCtrl.errorMsg = ""; - stopWatch(); - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - stopWatch(); - serverValidationManager.unsubscribe(currentProperty.alias, fieldName); - }); - } - }; -} +/** + * @ngdoc directive + * @name umbraco.directives.directive:valServer + * @restrict A + * @description This directive is used to associate a content property with a server-side validation response + * so that the validators in angular are updated based on server-side feedback. + **/ +function valServer(serverValidationManager) { + return { + require: ['ngModel', '?^umbProperty'], + restrict: "A", + link: function (scope, element, attr, ctrls) { + + var modelCtrl = ctrls[0]; + var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; + if (!umbPropCtrl) { + //we cannot proceed, this validator will be disabled + return; + } + + var watcher = null; + + //Need to watch the value model for it to change, previously we had subscribed to + //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that + // doesn't specifically have a 2 way ng binding. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + function startWatch() { + //if there's not already a watch + if (!watcher) { + watcher = scope.$watch(function () { + return modelCtrl.$modelValue; + }, function (newValue, oldValue) { + + if (!newValue || angular.equals(newValue, oldValue)) { + return; + } + + if (modelCtrl.$invalid) { + modelCtrl.$setValidity('valServer', true); + stopWatch(); + } + }, true); + } + } + + function stopWatch() { + if (watcher) { + watcher(); + watcher = null; + } + } + + var currentProperty = umbPropCtrl.property; + + //default to 'value' if nothing is set + var fieldName = "value"; + if (attr.valServer) { + fieldName = scope.$eval(attr.valServer); + if (!fieldName) { + //eval returned nothing so just use the string + fieldName = attr.valServer; + } + } + + //subscribe to the server validation changes + serverValidationManager.subscribe(currentProperty.alias, fieldName, function (isValid, propertyErrors, allErrors) { + if (!isValid) { + modelCtrl.$setValidity('valServer', false); + //assign an error msg property to the current validator + modelCtrl.errorMsg = propertyErrors[0].errorMsg; + startWatch(); + } + else { + modelCtrl.$setValidity('valServer', true); + //reset the error message + modelCtrl.errorMsg = ""; + stopWatch(); + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + stopWatch(); + serverValidationManager.unsubscribe(currentProperty.alias, fieldName); + }); + } + }; +} angular.module('umbraco.directives.validation').directive("valServer", valServer); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js index 9a077615df..1e0d2d8ba5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js @@ -1,54 +1,65 @@ -/** - * @ngdoc directive - * @name umbraco.directives.directive:valServerField - * @restrict A - * @description This directive is used to associate a content field (not user defined) with a server-side validation response - * so that the validators in angular are updated based on server-side feedback. - **/ -function valServerField(serverValidationManager) { - return { - require: 'ngModel', - restrict: "A", - link: function (scope, element, attr, ctrl) { - - if (!attr.valServerField) { - throw "valServerField must have a field name for referencing server errors"; - } - - var fieldName = attr.valServerField; - - //subscribe to the changed event of the view model. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - ctrl.$viewChangeListeners.push(function () { - if (ctrl.$invalid) { - ctrl.$setValidity('valServerField', true); - } - }); - - //subscribe to the server validation changes - serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { - if (!isValid) { - ctrl.$setValidity('valServerField', false); - //assign an error msg property to the current validator - ctrl.errorMsg = fieldErrors[0].errorMsg; - } - else { - ctrl.$setValidity('valServerField', true); - //reset the error message - ctrl.errorMsg = ""; - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - serverValidationManager.unsubscribe(null, fieldName); - }); - } - }; -} -angular.module('umbraco.directives.validation').directive("valServerField", valServerField); \ No newline at end of file +/** + * @ngdoc directive + * @name umbraco.directives.directive:valServerField + * @restrict A + * @description This directive is used to associate a content field (not user defined) with a server-side validation response + * so that the validators in angular are updated based on server-side feedback. + **/ +function valServerField(serverValidationManager) { + return { + require: 'ngModel', + restrict: "A", + link: function (scope, element, attr, ctrl) { + + var fieldName = null; + var eventBindings = []; + + attr.$observe("valServerField", function (newVal) { + if (newVal && fieldName === null) { + fieldName = newVal; + + //subscribe to the changed event of the view model. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + eventBindings.push(scope.$watch('ngModel', function(newValue){ + if (ctrl.$invalid) { + ctrl.$setValidity('valServerField', true); + } + })); + + //subscribe to the server validation changes + serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { + if (!isValid) { + ctrl.$setValidity('valServerField', false); + //assign an error msg property to the current validator + ctrl.errorMsg = fieldErrors[0].errorMsg; + } + else { + ctrl.$setValidity('valServerField', true); + //reset the error message + ctrl.errorMsg = ""; + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + serverValidationManager.unsubscribe(null, fieldName); + }); + } + }); + + scope.$on('$destroy', function(){ + // unbind watchers + for(var e in eventBindings) { + eventBindings[e](); + } + }); + + } + }; +} +angular.module('umbraco.directives.validation').directive("valServerField", valServerField); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js new file mode 100644 index 0000000000..916b933063 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js @@ -0,0 +1,49 @@ +/** +* @ngdoc directive +* @name umbraco.directives.directive:valSubView +* @restrict A +* @description Used to show validation warnings for a editor sub view to indicate that the section content has validation errors in its data. +* In order for this directive to work, the valFormManager directive must be placed on the containing form. +**/ +(function() { + 'use strict'; + + function valSubViewDirective() { + + function link(scope, el, attr, ctrl) { + + var valFormManager = ctrl[1]; + scope.subView.hasError = false; + + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + + var subViewContent = el.find(".ng-invalid"); + + if (subViewContent.length > 0) { + scope.subView.hasError = true; + } else { + scope.subView.hasError = false; + } + + } + else { + scope.subView.hasError = false; + } + }); + + } + + var directive = { + require: ['^form', '^valFormManager'], + restrict: "A", + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('valSubView', valSubViewDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js index cd6dc51eca..8d1fc60083 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js @@ -1,38 +1,38 @@ - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valTab -* @restrict A -* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. -* In order for this directive to work, the valFormManager directive must be placed on the containing form. -**/ -function valTab() { - return { - require: "^form", - restrict: "A", - link: function (scope, element, attr, formCtrl) { - - var tabId = "tab" + scope.tab.id; - - scope.tabHasError = false; - - //listen for form validation changes - scope.$on("valStatusChanged", function(evt, args) { - if (!args.form.$valid) { - var tabContent = element.closest(".umb-panel").find("#" + tabId); - //check if the validation messages are contained inside of this tabs - if (tabContent.find(".ng-invalid").length > 0) { - scope.tabHasError = true; - } else { - scope.tabHasError = false; - } - } - else { - scope.tabHasError = false; - } - }); - - } - }; -} + +/** +* @ngdoc directive +* @name umbraco.directives.directive:valTab +* @restrict A +* @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. +* In order for this directive to work, the valFormManager directive must be placed on the containing form. +**/ +function valTab() { + return { + require: ['^form', '^valFormManager'], + restrict: "A", + link: function (scope, element, attr, ctrs) { + + var valFormManager = ctrs[1]; + var tabId = "tab" + scope.tab.id; + scope.tabHasError = false; + + //listen for form validation changes + valFormManager.onValidationStatusChanged(function (evt, args) { + if (!args.form.$valid) { + var tabContent = element.closest(".umb-panel").find("#" + tabId); + //check if the validation messages are contained inside of this tabs + if (tabContent.find(".ng-invalid").length > 0) { + scope.tabHasError = true; + } else { + scope.tabHasError = false; + } + } + else { + scope.tabHasError = false; + } + }); + + } + }; +} angular.module('umbraco.directives.validation').directive("valTab", valTab); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtogglemsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtogglemsg.directive.js index 43792a708a..304f151274 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtogglemsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtogglemsg.directive.js @@ -1,90 +1,90 @@ -function valToggleMsg(serverValidationManager) { - return { - require: "^form", - restrict: "A", - - /** - Our directive requries a reference to a form controller which gets passed in to this parameter - */ - link: function (scope, element, attr, formCtrl) { - - if (!attr.valToggleMsg){ - throw "valToggleMsg requires that a reference to a validator is specified"; - } - if (!attr.valMsgFor){ - throw "valToggleMsg requires that the attribute valMsgFor exists on the element"; - } - if (!formCtrl[attr.valMsgFor]) { - throw "valToggleMsg cannot find field " + attr.valMsgFor + " on form " + formCtrl.$name; - } - - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - var hasCustomMsg = element.contents().length > 0; - - //add a watch to the validator for the value (i.e. myForm.value.$error.required ) - scope.$watch(function () { - //sometimes if a dialog closes in the middle of digest we can get null references here - - return (formCtrl && formCtrl[attr.valMsgFor]) ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null; - }, function () { - //sometimes if a dialog closes in the middle of digest we can get null references here - if ((formCtrl && formCtrl[attr.valMsgFor])) { - if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) { - element.show(); - //display the error message if this element has no contents - if (!hasCustomMsg) { - element.html(formCtrl[attr.valMsgFor].errorMsg); - } - } - else { - element.hide(); - } - } - }); - - var unsubscribe = []; - - //listen for the saving event (the result is a callback method which is called to unsubscribe) - unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { - showValidation = true; - if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) { - element.show(); - //display the error message if this element has no contents - if (!hasCustomMsg) { - element.html(formCtrl[attr.valMsgFor].errorMsg); - } - } - else { - element.hide(); - } - })); - - //listen for the saved event (the result is a callback method which is called to unsubscribe) - unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { - showValidation = false; - element.hide(); - })); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom - // element might still be there even after the modal has been hidden. - element.bind('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - } - }; -} - -/** -* @ngdoc directive -* @name umbraco.directives.directive:valToggleMsg -* @restrict A -* @element input -* @requires formController -* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ? -**/ +function valToggleMsg(serverValidationManager) { + return { + require: "^form", + restrict: "A", + + /** + Our directive requries a reference to a form controller which gets passed in to this parameter + */ + link: function (scope, element, attr, formCtrl) { + + if (!attr.valToggleMsg){ + throw "valToggleMsg requires that a reference to a validator is specified"; + } + if (!attr.valMsgFor){ + throw "valToggleMsg requires that the attribute valMsgFor exists on the element"; + } + if (!formCtrl[attr.valMsgFor]) { + throw "valToggleMsg cannot find field " + attr.valMsgFor + " on form " + formCtrl.$name; + } + + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + var hasCustomMsg = element.contents().length > 0; + + //add a watch to the validator for the value (i.e. myForm.value.$error.required ) + scope.$watch(function () { + //sometimes if a dialog closes in the middle of digest we can get null references here + + return (formCtrl && formCtrl[attr.valMsgFor]) ? formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] : null; + }, function () { + //sometimes if a dialog closes in the middle of digest we can get null references here + if ((formCtrl && formCtrl[attr.valMsgFor])) { + if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg] && showValidation) { + element.show(); + //display the error message if this element has no contents + if (!hasCustomMsg) { + element.html(formCtrl[attr.valMsgFor].errorMsg); + } + } + else { + element.hide(); + } + } + }); + + var unsubscribe = []; + + //listen for the saving event (the result is a callback method which is called to unsubscribe) + unsubscribe.push(scope.$on("formSubmitting", function(ev, args) { + showValidation = true; + if (formCtrl[attr.valMsgFor].$error[attr.valToggleMsg]) { + element.show(); + //display the error message if this element has no contents + if (!hasCustomMsg) { + element.html(formCtrl[attr.valMsgFor].errorMsg); + } + } + else { + element.hide(); + } + })); + + //listen for the saved event (the result is a callback method which is called to unsubscribe) + unsubscribe.push(scope.$on("formSubmitted", function(ev, args) { + showValidation = false; + element.hide(); + })); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise if this directive is part of a modal, the listener still exists because the dom + // element might still be there even after the modal has been hidden. + element.bind('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + + } + }; +} + +/** +* @ngdoc directive +* @name umbraco.directives.directive:valToggleMsg +* @restrict A +* @element input +* @requires formController +* @description This directive will show/hide an error based on: is the value + the given validator invalid? AND, has the form been submitted ? +**/ angular.module('umbraco.directives.validation').directive("valToggleMsg", valToggleMsg); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js new file mode 100644 index 0000000000..13f603260d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js @@ -0,0 +1,26 @@ +angular.module("umbraco.filters") + .filter('compareArrays', function() { + return function inArray(array, compareArray, compareProperty) { + + var result = []; + + angular.forEach(array, function(arrayItem){ + + var exists = false; + + angular.forEach(compareArray, function(compareItem){ + if( arrayItem[compareProperty] === compareItem[compareProperty]) { + exists = true; + } + }); + + if(!exists) { + result.push(arrayItem); + } + + }); + + return result; + + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js index 1ecc0b5cf8..9ad97899e5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/tree.mocks.js @@ -157,7 +157,7 @@ angular.module('umbraco.mocks'). name: "developer", id: -1, children: [ - { name: "Data types", childNodesUrl: dataTypeChildrenUrl, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: dataTypeMenuUrl, metaData: { treeAlias: "datatype" } }, + { name: "Data types", childNodesUrl: dataTypeChildrenUrl, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: dataTypeMenuUrl, metaData: { treeAlias: "dataTypes" } }, { name: "Macros", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "macros" } }, { name: "Packages", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "packager" } }, { name: "XSLT Files", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "xslt" } }, @@ -179,7 +179,7 @@ angular.module('umbraco.mocks'). { name: "Templates", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "templates" } }, { name: "Dictionary", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "dictionary" } }, { name: "Media types", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "mediaTypes" } }, - { name: "Document types", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "nodeTypes" } } + { name: "Document types", childNodesUrl: url, id: -1, icon: "icon-folder-close", children: [], expanded: false, hasChildren: true, level: 1, menuUrl: menuUrl, metaData: { treeAlias: "documentTypes" } } ], expanded: true, hasChildren: true, diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/externalLoginInfo.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/externalLoginInfo.mocks.js new file mode 100644 index 0000000000..f054c8493e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/externalLoginInfo.mocks.js @@ -0,0 +1,8 @@ +angular.module("umbraco.mocks").factory('externalLoginInfo', + function () { + return { + errors: [], + providers: [] + }; + } +); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index aefb678ffe..6365606a5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -716,7 +716,7 @@ angular.module('umbraco.mocks'). "user_administrators": "Administrator", "user_categoryField": "Category field", "user_changePassword": "Change Your Password", - "user_newPassword": "Change Your Password", + "user_newPassword": "New password", "user_confirmNewPassword": "Confirm new password", "user_changePasswordDescription": "You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button", "user_contentChannel": "Content Channel", @@ -730,6 +730,7 @@ angular.module('umbraco.mocks'). "user_mediastartnode": "Start Node in Media Library", "user_modules": "Sections", "user_noConsole": "Disable Umbraco Access", + "user_oldPassword": "Old password", "user_password": "Password", "user_resetPassword": "Reset password", "user_passwordChanged": "Your password has been changed!", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index f32602bda6..20ebaa10c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -51,7 +51,162 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { }), 'Login failed for user ' + username); }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performRequestPasswordReset + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided email address is a valid user account and sends a link + * to allow them to reset their password + * + * ##usage + *
      +         * authResource.performRequestPasswordReset(email)
      +         *    .then(function(data) {
      +         *        //Do stuff for password reset request...
      +         *    });
      +         * 
      + * @param {string} email Email address of backoffice user + * @returns {Promise} resourcePromise object + * + */ + performRequestPasswordReset: function (email) { + + if (!email) { + return angularHelper.rejectedPromise({ + errorMsg: 'Email address cannot be empty' + }); + } + + //TODO: This validation shouldn't really be done here, the validation on the login dialog + // is pretty hacky which is why this is here, ideally validation on the login dialog would + // be done properly. + var emailRegex = /\S+@\S+\.\S+/; + if (!emailRegex.test(email)) { + return angularHelper.rejectedPromise({ + errorMsg: 'Email address is not valid' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostRequestPasswordReset"), { + email: email + }), + 'Request password reset failed for email ' + email); + }, + /** + * @ngdoc method + * @name umbraco.resources.authResource#performValidatePasswordResetCode + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid + * + * ##usage + *
      +         * authResource.performValidatePasswordResetCode(resetCode)
      +         *    .then(function(data) {
      +         *        //Allow reset of password
      +         *    });
      +         * 
      + * @param {integer} userId User Id + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performValidatePasswordResetCode: function (userId, resetCode) { + + if (!userId) { + return angularHelper.rejectedPromise({ + errorMsg: 'User Id cannot be empty' + }); + } + + if (!resetCode) { + return angularHelper.rejectedPromise({ + errorMsg: 'Reset code cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostValidatePasswordResetCode"), + { + userId: userId, + resetCode: resetCode + }), + 'Password reset code validation failed for userId ' + userId + ', code' + resetCode); + }, + + /** + * @ngdoc method + * @name umbraco.resources.authResource#performSetPassword + * @methodOf umbraco.resources.authResource + * + * @description + * Checks to see if the provided password reset code is valid and sets the user's password + * + * ##usage + *
      +         * authResource.performSetPassword(userId, password, confirmPassword, resetCode)
      +         *    .then(function(data) {
      +         *        //Password set
      +         *    });
      +         * 
      + * @param {integer} userId User Id + * @param {string} password New password + * @param {string} confirmPassword Confirmation of new password + * @param {string} resetCode Password reset code + * @returns {Promise} resourcePromise object + * + */ + performSetPassword: function (userId, password, confirmPassword, resetCode) { + + if (userId === undefined || userId === null) { + return angularHelper.rejectedPromise({ + errorMsg: 'User Id cannot be empty' + }); + } + + if (!password) { + return angularHelper.rejectedPromise({ + errorMsg: 'Password cannot be empty' + }); + } + + if (password !== confirmPassword) { + return angularHelper.rejectedPromise({ + errorMsg: 'Password and confirmation do not match' + }); + } + + if (!resetCode) { + return angularHelper.rejectedPromise({ + errorMsg: 'Reset code cannot be empty' + }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "PostSetPassword"), + { + userId: userId, + password: password, + resetCode: resetCode + }), + 'Password reset code validation failed for userId ' + userId); + }, + unlinkLogin: function (loginProvider, providerKey) { if (!loginProvider || !providerKey) { return angularHelper.rejectedPromise({ diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 86584d41f7..3c1c1e2252 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -29,8 +29,8 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { function saveContentItem(content, action, files) { return umbRequestHelper.postSaveContent({ restApiUrl: umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostSave"), + "contentApiBaseUrl", + "PostSave"), content: content, action: action, files: files, @@ -41,29 +41,38 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { } return { - + + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for content recycle bin'); + }, + /** - * @ngdoc method - * @name umbraco.resources.contentResource#sort - * @methodOf umbraco.resources.contentResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
      -         * var ids = [123,34533,2334,23434];
      -         * contentResource.sort({ parentId: 1244, sortedIds: ids })
      -         *    .then(function() {
      -         *        $scope.complete = true;
      -         *    });
      -         * 
      - * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#sort + * @methodOf umbraco.resources.contentResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
      +          * var ids = [123,34533,2334,23434];
      +          * contentResource.sort({ parentId: 1244, sortedIds: ids })
      +          *    .then(function() {
      +          *        $scope.complete = true;
      +          *    });
      +          * 
      + * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ sort: function (args) { if (!args) { throw "args cannot be null"; @@ -76,37 +85,37 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort content'); + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort content'); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#move - * @methodOf umbraco.resources.contentResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
      -         * contentResource.move({ parentId: 1244, id: 123 })
      -         *    .then(function() {
      -         *        alert("node was moved");
      -         *    }, function(err){
      -         *      alert("node didnt move:" + err.data.Message); 
      -         *    });
      -         * 
      - * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#move + * @methodOf umbraco.resources.contentResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
      +          * contentResource.move({ parentId: 1244, id: 123 })
      +          *    .then(function() {
      +          *        alert("node was moved");
      +          *    }, function(err){
      +          *      alert("node didnt move:" + err.data.Message); 
      +          *    });
      +          * 
      + * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ move: function (args) { if (!args) { throw "args cannot be null"; @@ -119,38 +128,38 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move content'); + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#copy - * @methodOf umbraco.resources.contentResource - * - * @description - * Copies a node underneath a new parentId - * - * ##usage - *
      -         * contentResource.copy({ parentId: 1244, id: 123 })
      -         *    .then(function() {
      -         *        alert("node was copied");
      -         *    }, function(err){
      -         *      alert("node wasnt copy:" + err.data.Message); 
      -         *    });
      -         * 
      - * @param {Object} args arguments object - * @param {Int} args.id the ID of the node to copy - * @param {Int} args.parentId the ID of the parent node to copy to - * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#copy + * @methodOf umbraco.resources.contentResource + * + * @description + * Copies a node underneath a new parentId + * + * ##usage + *
      +          * contentResource.copy({ parentId: 1244, id: 123 })
      +          *    .then(function() {
      +          *        alert("node was copied");
      +          *    }, function(err){
      +          *      alert("node wasnt copy:" + err.data.Message); 
      +          *    });
      +          * 
      + * @param {Object} args arguments object + * @param {Int} args.id the ID of the node to copy + * @param {Int} args.parentId the ID of the parent node to copy to + * @param {Boolean} args.relateToOriginal if true, relates the copy to the original through the relation api + * @returns {Promise} resourcePromise object. + * + */ copy: function (args) { if (!args) { throw "args cannot be null"; @@ -163,271 +172,271 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), - args), - 'Failed to copy content'); + $http.post(umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostCopy"), + args), + 'Failed to copy content'); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#unPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Unpublishes a content item with a given Id - * - * ##usage - *
      -         * contentResource.unPublish(1234)
      -         *    .then(function() {
      -         *        alert("node was unpulished");
      -         *    }, function(err){
      -         *      alert("node wasnt unpublished:" + err.data.Message); 
      -         *    });
      -         * 
      - * @param {Int} id the ID of the node to unpublish - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#unPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Unpublishes a content item with a given Id + * + * ##usage + *
      +          * contentResource.unPublish(1234)
      +          *    .then(function() {
      +          *        alert("node was unpulished");
      +          *    }, function(err){
      +          *      alert("node wasnt unpublished:" + err.data.Message); 
      +          *    });
      +          * 
      + * @param {Int} id the ID of the node to unpublish + * @returns {Promise} resourcePromise object. + * + */ unPublish: function (id) { if (!id) { throw "id cannot be null"; } - + return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostUnPublish", - [{ id: id }])), - 'Failed to publish content with id ' + id); + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostUnPublish", + [{ id: id }])), + 'Failed to publish content with id ' + id); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#emptyRecycleBin - * @methodOf umbraco.resources.contentResource - * - * @description - * Empties the content recycle bin - * - * ##usage - *
      -         * contentResource.emptyRecycleBin()
      -         *    .then(function() {
      -         *        alert('its empty!');
      -         *    });
      -         * 
      - * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function() { + * @ngdoc method + * @name umbraco.resources.contentResource#emptyRecycleBin + * @methodOf umbraco.resources.contentResource + * + * @description + * Empties the content recycle bin + * + * ##usage + *
      +          * contentResource.emptyRecycleBin()
      +          *    .then(function() {
      +          *        alert('its empty!');
      +          *    });
      +          * 
      + * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#deleteById - * @methodOf umbraco.resources.contentResource - * - * @description - * Deletes a content item with a given id - * - * ##usage - *
      -         * contentResource.deleteById(1234)
      -         *    .then(function() {
      -         *        alert('its gone!');
      -         *    });
      -         * 
      - * - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function(id) { + * @ngdoc method + * @name umbraco.resources.contentResource#deleteById + * @methodOf umbraco.resources.contentResource + * + * @description + * Deletes a content item with a given id + * + * ##usage + *
      +          * contentResource.deleteById(1234)
      +          *    .then(function() {
      +          *        alert('its gone!');
      +          *    });
      +          * 
      + * + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getById - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets a content item with a given id - * - * ##usage - *
      -         * contentResource.getById(1234)
      -         *    .then(function(content) {
      -         *        var myDoc = content; 
      -         *        alert('its here!');
      -         *    });
      -         * 
      - * - * @param {Int} id id of content item to return - * @returns {Promise} resourcePromise object containing the content item. - * - */ - getById: function (id) { + * @ngdoc method + * @name umbraco.resources.contentResource#getById + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets a content item with a given id + * + * ##usage + *
      +          * contentResource.getById(1234)
      +          *    .then(function(content) {
      +          *        var myDoc = content; 
      +          *        alert('its here!');
      +          *    });
      +          * 
      + * + * @param {Int} id id of content item to return + * @returns {Promise} resourcePromise object containing the content item. + * + */ + getById: function (id) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for content id ' + id); + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for content id ' + id); }, - + /** - * @ngdoc method - * @name umbraco.resources.contentResource#getByIds - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets an array of content items, given a collection of ids - * - * ##usage - *
      -         * contentResource.getByIds( [1234,2526,28262])
      -         *    .then(function(contentArray) {
      -         *        var myDoc = contentArray; 
      -         *        alert('they are here!');
      -         *    });
      -         * 
      - * - * @param {Array} ids ids of content items to return as an array - * @returns {Promise} resourcePromise object containing the content items array. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#getByIds + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets an array of content items, given a collection of ids + * + * ##usage + *
      +          * contentResource.getByIds( [1234,2526,28262])
      +          *    .then(function(contentArray) {
      +          *        var myDoc = contentArray; 
      +          *        alert('they are here!');
      +          *    });
      +          * 
      + * + * @param {Array} ids ids of content items to return as an array + * @returns {Promise} resourcePromise object containing the content items array. + * + */ getByIds: function (ids) { - + var idQuery = ""; - _.each(ids, function(item) { + _.each(ids, function (item) { idQuery += "ids=" + item + "&"; }); return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for content with multiple ids'); + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for content with multiple ids'); }, - + /** - * @ngdoc method - * @name umbraco.resources.contentResource#getScaffold - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. - * - * - Parent Id must be provided so umbraco knows where to store the content - * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold - * - * The scaffold is used to build editors for content that has not yet been populated with data. - * - * ##usage - *
      -         * contentResource.getScaffold(1234, 'homepage')
      -         *    .then(function(scaffold) {
      -         *        var myDoc = scaffold;
      -         *        myDoc.name = "My new document"; 
      -         *
      -         *        contentResource.publish(myDoc, true)
      -         *            .then(function(content){
      -         *                alert("Retrieved, updated and published again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Int} parentId id of content item to return - * @param {String} alias contenttype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the content scaffold. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#getScaffold + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a scaffold of an empty content item, given the id of the content item to place it underneath and the content type alias. + * + * - Parent Id must be provided so umbraco knows where to store the content + * - Content Type alias must be provided so umbraco knows which properties to put on the content scaffold + * + * The scaffold is used to build editors for content that has not yet been populated with data. + * + * ##usage + *
      +          * contentResource.getScaffold(1234, 'homepage')
      +          *    .then(function(scaffold) {
      +          *        var myDoc = scaffold;
      +          *        myDoc.name = "My new document"; 
      +          *
      +          *        contentResource.publish(myDoc, true)
      +          *            .then(function(content){
      +          *                alert("Retrieved, updated and published again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Int} parentId id of content item to return + * @param {String} alias contenttype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the content scaffold. + * + */ getScaffold: function (parentId, alias) { - + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty content item type ' + alias); + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty content item type ' + alias); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getNiceUrl - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns a url, given a node ID - * - * ##usage - *
      -         * contentResource.getNiceUrl(id)
      -         *    .then(function(url) {
      -         *        alert('its here!');
      -         *    });
      -         * 
      - * - * @param {Int} id Id of node to return the public url to - * @returns {Promise} resourcePromise object containing the url. - * - */ - getNiceUrl: function (id) { + * @ngdoc method + * @name umbraco.resources.contentResource#getNiceUrl + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns a url, given a node ID + * + * ##usage + *
      +          * contentResource.getNiceUrl(id)
      +          *    .then(function(url) {
      +          *        alert('its here!');
      +          *    });
      +          * 
      + * + * @param {Int} id Id of node to return the public url to + * @returns {Promise} resourcePromise object containing the url. + * + */ + getNiceUrl: function (id) { return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetNiceUrl",[{id: id}])), - 'Failed to retrieve url for id:' + id); + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetNiceUrl", [{ id: id }])), + 'Failed to retrieve url for id:' + id); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#getChildren - * @methodOf umbraco.resources.contentResource - * - * @description - * Gets children of a content item with a given id - * - * ##usage - *
      -         * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
      -         *    .then(function(contentArray) {
      -         *        var children = contentArray; 
      -         *        alert('they are here!');
      -         *    });
      -         * 
      - * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#getChildren + * @methodOf umbraco.resources.contentResource + * + * @description + * Gets children of a content item with a given id + * + * ##usage + *
      +          * contentResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
      +          *    .then(function(contentArray) {
      +          *        var children = contentArray; 
      +          *        alert('they are here!');
      +          *    });
      +          * 
      + * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ getChildren: function (parentId, options) { var defaults = { @@ -435,10 +444,11 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { pageNumber: 0, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + orderBySystemField: true }; if (options === undefined) { - options = {}; + options = {}; } //overwrite the defaults if there are any specified angular.extend(defaults, options); @@ -452,184 +462,209 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { options.orderDirection = "Descending"; } + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ])), - 'Failed to retrieve children for content item ' + parentId); + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ])), + 'Failed to retrieve children for content item ' + parentId); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#hasPermission - * @methodOf umbraco.resources.contentResource - * - * @description - * Returns true/false given a permission char to check against a nodeID - * for the current user - * - * ##usage - *
      -         * contentResource.hasPermission('p',1234)
      -         *    .then(function() {
      -         *        alert('You are allowed to publish this item');
      -         *    });
      -         * 
      - * - * @param {String} permission char representing the permission to check - * @param {Int} id id of content item to delete - * @returns {Promise} resourcePromise object. - * - */ - checkPermission: function(permission, id) { + * @ngdoc method + * @name umbraco.resources.contentResource#hasPermission + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns true/false given a permission char to check against a nodeID + * for the current user + * + * ##usage + *
      +          * contentResource.hasPermission('p',1234)
      +          *    .then(function() {
      +          *        alert('You are allowed to publish this item');
      +          *    });
      +          * 
      + * + * @param {String} permission char representing the permission to check + * @param {Int} id id of content item to delete + * @returns {Promise} resourcePromise object. + * + */ + checkPermission: function (permission, id) { return umbRequestHelper.resourcePromise( - $http.get( + $http.get( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "HasPermission", + [{ permissionToCheck: permission }, { nodeId: id }])), + 'Failed to check permission for item ' + id); + }, + + getPermissions: function (nodeIds) { + return umbRequestHelper.resourcePromise( + $http.post( umbRequestHelper.getApiUrl( "contentApiBaseUrl", - "HasPermission", - [{ permissionToCheck: permission },{ nodeId: id }])), - 'Failed to check permission for item ' + id); + "GetPermissions"), + nodeIds), + 'Failed to get permissions'); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#save - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
      -         * contentResource.getById(1234)
      -         *    .then(function(content) {
      -         *          content.name = "I want a new name!";
      -         *          contentResource.save(content, false)
      -         *            .then(function(content){
      -         *                alert("Retrieved, updated and saved again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#save + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item to its current version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
      +          * contentResource.getById(1234)
      +          *    .then(function(content) {
      +          *          content.name = "I want a new name!";
      +          *          contentResource.save(content, false)
      +          *            .then(function(content){
      +          *                alert("Retrieved, updated and saved again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ save: function (content, isNew, files) { return saveContentItem(content, "save" + (isNew ? "New" : ""), files); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#publish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation - * if the content item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
      -         * contentResource.getById(1234)
      -         *    .then(function(content) {
      -         *          content.name = "I want a new name, and be published!";
      -         *          contentResource.publish(content, false)
      -         *            .then(function(content){
      -         *                alert("Retrieved, updated and published again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#publish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves and publishes changes made to a content item to a new version, if the content item is new, the isNew paramater must be passed to force creation + * if the content item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
      +          * contentResource.getById(1234)
      +          *    .then(function(content) {
      +          *          content.name = "I want a new name, and be published!";
      +          *          contentResource.publish(content, false)
      +          *            .then(function(content){
      +          *                alert("Retrieved, updated and published again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ publish: function (content, isNew, files) { return saveContentItem(content, "publish" + (isNew ? "New" : ""), files); }, - + /** - * @ngdoc method - * @name umbraco.resources.contentResource#sendToPublish - * @methodOf umbraco.resources.contentResource - * - * @description - * Saves changes made to a content item, and notifies any subscribers about a pending publication - * - * ##usage - *
      -         * contentResource.getById(1234)
      -         *    .then(function(content) {
      -         *          content.name = "I want a new name, and be published!";
      -         *          contentResource.sendToPublish(content, false)
      -         *            .then(function(content){
      -         *                alert("Retrieved, updated and notication send off");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Object} content The content item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the document - * @returns {Promise} resourcePromise object containing the saved content item. - * - */ + * @ngdoc method + * @name umbraco.resources.contentResource#sendToPublish + * @methodOf umbraco.resources.contentResource + * + * @description + * Saves changes made to a content item, and notifies any subscribers about a pending publication + * + * ##usage + *
      +          * contentResource.getById(1234)
      +          *    .then(function(content) {
      +          *          content.name = "I want a new name, and be published!";
      +          *          contentResource.sendToPublish(content, false)
      +          *            .then(function(content){
      +          *                alert("Retrieved, updated and notication send off");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Object} content The content item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the document + * @returns {Promise} resourcePromise object containing the saved content item. + * + */ sendToPublish: function (content, isNew, files) { return saveContentItem(content, "sendPublish" + (isNew ? "New" : ""), files); }, /** - * @ngdoc method - * @name umbraco.resources.contentResource#publishByid - * @methodOf umbraco.resources.contentResource - * - * @description - * Publishes a content item with a given ID - * - * ##usage - *
      -         * contentResource.publishById(1234)
      -         *    .then(function(content) {
      -         *        alert("published");
      -         *    });
      -         * 
      - * - * @param {Int} id The ID of the conten to publish - * @returns {Promise} resourcePromise object containing the published content item. - * - */ - publishById: function(id){ + * @ngdoc method + * @name umbraco.resources.contentResource#publishByid + * @methodOf umbraco.resources.contentResource + * + * @description + * Publishes a content item with a given ID + * + * ##usage + *
      +          * contentResource.publishById(1234)
      +          *    .then(function(content) {
      +          *        alert("published");
      +          *    });
      +          * 
      + * + * @param {Int} id The ID of the conten to publish + * @returns {Promise} resourcePromise object containing the published content item. + * + */ + publishById: function (id) { if (!id) { throw "id cannot be null"; } - + return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "contentApiBaseUrl", - "PostPublishById", - [{ id: id }])), - 'Failed to publish content with id ' + id); - + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "PostPublishById", + [{ id: id }])), + 'Failed to publish content with id ' + id); + } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index deaa857e94..4c7b6f916e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -3,22 +3,43 @@ * @name umbraco.resources.contentTypeResource * @description Loads in data for content types **/ -function contentTypeResource($q, $http, umbRequestHelper) { +function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { - getAssignedListViewDataType: function (contentTypeId) { - + getCount: function () { return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", - "GetAssignedListViewDataType", - [{ contentTypeId: contentTypeId }])), - 'Failed to retrieve data for content id ' + contentTypeId); + "GetCount")), + 'Failed to retrieve count'); }, - + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + + var query = { + contentTypeId: contentTypeId, + filterContentTypes: filterContentTypes, + filterPropertyTypes: filterPropertyTypes + }; + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "GetAvailableCompositeContentTypes"), + query), + 'Failed to retrieve data for content type id ' + contentTypeId); + }, + + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#getAllowedTypes @@ -33,22 +54,23 @@ function contentTypeResource($q, $http, umbRequestHelper) { * .then(function(array) { * $scope.type = type; * }); - * - * @param {Int} contentId id of the content item to retrive allowed child types for + * + * @param {Int} contentTypeId id of the content item to retrive allowed child types for * @returns {Promise} resourcePromise object. * */ - getAllowedTypes: function (contentId) { - + getAllowedTypes: function (contentTypeId) { + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", "GetAllowedChildren", - [{ contentId: contentId }])), - 'Failed to retrieve data for content id ' + contentId); + [{ contentId: contentTypeId }])), + 'Failed to retrieve data for content id ' + contentTypeId); }, + /** * @ngdoc method * @name umbraco.resources.contentTypeResource#getAllPropertyTypeAliases @@ -56,7 +78,7 @@ function contentTypeResource($q, $http, umbRequestHelper) { * * @description * Returns a list of defined property type aliases - * + * * @returns {Promise} resourcePromise object. * */ @@ -68,6 +90,172 @@ function contentTypeResource($q, $http, umbRequestHelper) { "contentTypeApiBaseUrl", "GetAllPropertyTypeAliases")), 'Failed to retrieve property type aliases'); + }, + + getPropertyTypeScaffold : function (id) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "GetPropertyTypeScaffold", + [{ id: id }])), + 'Failed to retrieve property type scaffold'); + }, + + getById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve content type'); + }, + + deleteById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete content type'); + }, + + deleteContainerById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "DeleteContainer", + [{ id: id }])), + 'Failed to delete content type contaier'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#getAll + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Returns a list of all content types + * + * @returns {Promise} resourcePromise object. + * + */ + getAll: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "GetAll")), + 'Failed to retrieve all content types'); + }, + + getScaffold: function (parentId) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "contentTypeApiBaseUrl", + "GetEmpty", { parentId: parentId })), + 'Failed to retrieve content type scaffold'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#save + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Saves or update a content type + * + * @param {Object} content data type object to create/update + * @returns {Promise} resourcePromise object. + * + */ + save: function (contentType) { + + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostSave"), saveModel), + 'Failed to save data for content type id ' + contentType.id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#move + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
      +         * contentTypeResource.move({ parentId: 1244, id: 123 })
      +         *    .then(function() {
      +         *        alert("node was moved");
      +         *    }, function(err){
      +         *      alert("node didnt move:" + err.data.Message);
      +         *    });
      +         * 
      + * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, + + copy: function(args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to copy content'); + }, + + createContainer: function(parentId, name) { + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateContainer", { parentId: parentId, name: name })), + 'Failed to create a folder under parent id ' + parentId); + } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js index 3bd8548990..7bc65c89a6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js @@ -4,9 +4,9 @@ * @description Loads in data for data types **/ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { - + return { - + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#getPreValues @@ -17,17 +17,17 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * * ##usage *
      -         * dataTypeResource.getPrevalyes("Umbraco.MediaPicker", 1234)
      +         * dataTypeResource.getPreValues("Umbraco.MediaPicker", 1234)
                *    .then(function(prevalues) {
                *        alert('its gone!');
                *    });
      -         * 
      - * + * + * * @param {String} editorAlias string alias of editor type to retrive prevalues configuration for - * @param {Int} id id of datatype to retrieve prevalues for + * @param {Int} id id of datatype to retrieve prevalues for * @returns {Promise} resourcePromise object. * - */ + */ getPreValues: function (editorAlias, dataTypeId) { if (!dataTypeId) { @@ -40,7 +40,7 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { "dataTypeApiBaseUrl", "GetPreValues", [{ editorAlias: editorAlias }, { dataTypeId: dataTypeId }])), - 'Failed to retrieve pre values for editor alias ' + editorAlias); + "Failed to retrieve pre values for editor alias " + editorAlias); }, /** @@ -54,34 +54,93 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * ##usage *
                * dataTypeResource.getById(1234)
      -         *    .then(function() {
      -         *        alert('its gone!');
      +         *    .then(function(datatype) {
      +         *        alert('its here!');
                *    });
      -         * 
      - * - * @param {Int} id id of data type to retrieve + * + * + * @param {Int} id id of data type to retrieve * @returns {Promise} resourcePromise object. * */ getById: function (id) { - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "dataTypeApiBaseUrl", "GetById", [{ id: id }])), - 'Failed to retrieve data for data type id ' + id); + "Failed to retrieve data for data type id " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.dataTypeResource#getByName + * @methodOf umbraco.resources.dataTypeResource + * + * @description + * Gets a data type item with a given name + * + * ##usage + *
      +         * dataTypeResource.getByName("upload")
      +         *    .then(function(datatype) {
      +         *        alert('its here!');
      +         *    });
      +         * 
      + * + * @param {String} name Name of data type to retrieve + * @returns {Promise} resourcePromise object. + * + */ + getByName: function (name) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "GetByName", + [{ name: name }])), + "Failed to retrieve data for data type with name: " + name); }, getAll: function () { - + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "dataTypeApiBaseUrl", "GetAll")), - 'Failed to retrieve data'); + "Failed to retrieve data"); + }, + + getGroupedDataTypes: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "GetGroupedDataTypes")), + "Failed to retrieve data"); + }, + + getGroupedPropertyEditors : function(){ + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "GetGroupedPropertyEditors")), + "Failed to retrieve data"); + }, + + getAllPropertyEditors: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "GetAllPropertyEditors")), + "Failed to retrieve data"); }, /** @@ -91,34 +150,34 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * * @description * Returns a scaffold of an empty data type item - * + * * The scaffold is used to build editors for data types that has not yet been populated with data. - * + * * ##usage *
                * dataTypeResource.getScaffold()
                *    .then(function(scaffold) {
                *        var myType = scaffold;
      -         *        myType.name = "My new data type"; 
      +         *        myType.name = "My new data type";
                *
                *        dataTypeResource.save(myType, myType.preValues, true)
                *            .then(function(type){
                *                alert("Retrieved, updated and saved again");
                *            });
                *    });
      -         * 
      - * + * + * * @returns {Promise} resourcePromise object containing the data type scaffold. * */ - getScaffold: function () { - + getScaffold: function (parentId) { + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "dataTypeApiBaseUrl", - "GetEmpty")), - 'Failed to retrieve data for empty datatype'); + "GetEmpty", { parentId: parentId })), + "Failed to retrieve data for empty datatype"); }, /** * @ngdoc method @@ -134,9 +193,9 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { * .then(function() { * alert('its gone!'); * }); - * - * - * @param {Int} id id of content item to delete + * + * + * @param {Int} id id of content item to delete * @returns {Promise} resourcePromise object. * */ @@ -147,32 +206,159 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { "dataTypeApiBaseUrl", "DeleteById", [{ id: id }])), - 'Failed to delete item ' + id); + "Failed to delete item " + id); }, - + + deleteContainerById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "DeleteContainer", + [{ id: id }])), + 'Failed to delete content type contaier'); + }, + + + + /** + * @ngdoc method + * @name umbraco.resources.dataTypeResource#getCustomListView + * @methodOf umbraco.resources.dataTypeResource + * + * @description + * Returns a custom listview, given a content types alias + * + * + * ##usage + *
      +         * dataTypeResource.getCustomListView("home")
      +         *    .then(function(listview) {
      +         *    });
      +         * 
      + * + * @returns {Promise} resourcePromise object containing the listview datatype. + * + */ + + getCustomListView: function (contentTypeAlias) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "GetCustomListView", + { contentTypeAlias: contentTypeAlias } + )), + "Failed to retrieve data for custom listview datatype"); + }, + + /** + * @ngdoc method + * @name umbraco.resources.dataTypeResource#createCustomListView + * @methodOf umbraco.resources.dataTypeResource + * + * @description + * Creates and returns a custom listview, given a content types alias + * + * ##usage + *
      +        * dataTypeResource.createCustomListView("home")
      +        *    .then(function(listview) {
      +        *    });
      +        * 
      + * + * @returns {Promise} resourcePromise object containing the listview datatype. + * + */ + createCustomListView: function (contentTypeAlias) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "PostCreateCustomListView", + { contentTypeAlias: contentTypeAlias } + )), + "Failed to create a custom listview datatype"); + }, + /** * @ngdoc method * @name umbraco.resources.dataTypeResource#save * @methodOf umbraco.resources.dataTypeResource * * @description - * Saves or update a data type - * + * Saves or update a data type + * * @param {Object} dataType data type object to create/update * @param {Array} preValues collection of prevalues on the datatype - * @param {Bool} isNew set to true if type should be create instead of updated + * @param {Bool} isNew set to true if type should be create instead of updated * @returns {Promise} resourcePromise object. * */ save: function (dataType, preValues, isNew) { - + var saveModel = umbDataFormatter.formatDataTypePostData(dataType, preValues, "save" + (isNew ? "New" : "")); return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl("dataTypeApiBaseUrl", "PostSave"), saveModel), - 'Failed to save data for data type id ' + dataType.id); + "Failed to save data for data type id " + dataType.id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.dataTypeResource#move + * @methodOf umbraco.resources.dataTypeResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
      +         * dataTypeResource.move({ parentId: 1244, id: 123 })
      +         *    .then(function() {
      +         *        alert("node was moved");
      +         *    }, function(err){
      +         *      alert("node didnt move:" + err.data.Message); 
      +         *    });
      +         * 
      + * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("dataTypeApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, + + createContainer: function (parentId, name) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "dataTypeApiBaseUrl", + "PostCreateContainer", + { parentId: parentId, name: name })), + 'Failed to create a folder under parent id ' + parentId); } }; } -angular.module('umbraco.resources').factory('dataTypeResource', dataTypeResource); +angular.module("umbraco.resources").factory("dataTypeResource", dataTypeResource); 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 c136f88bc7..a9296acc37 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 @@ -36,6 +36,16 @@ function entityResource($q, $http, umbRequestHelper) { //the factory object returned return { + getSafeAlias: function (value, camelCase) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetSafeAlias", { value: value, camelCase: camelCase })), + 'Failed to retrieve content type scaffold'); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getPath @@ -139,6 +149,12 @@ function entityResource($q, $http, umbRequestHelper) { _.each(ids, function(item) { query += "ids=" + item + "&"; }); + + // if ids array is empty we need a empty variable in the querystring otherwise the service returns a error + if (ids.length === 0) { + query += "ids=&"; + } + query += "type=" + type; return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js index 9ba64838c9..06b5f9496c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/macro.resource.js @@ -36,7 +36,7 @@ function macroResource($q, $http, umbRequestHelper) { * @methodOf umbraco.resources.macroResource * * @description - * Gets the result of a macro as html to display in the rich text editor + * Gets the result of a macro as html to display in the rich text editor or in the Grid * * @param {int} macroId The macro id to get parameters for * @param {int} pageId The current page id @@ -45,39 +45,17 @@ function macroResource($q, $http, umbRequestHelper) { */ getMacroResultAsHtmlForEditor: function (macroAlias, pageId, macroParamDictionary) { - //need to format the query string for the custom dictionary - var query = "macroAlias=" + macroAlias + "&pageId=" + pageId; - if (macroParamDictionary) { - var counter = 0; - _.each(macroParamDictionary, function (val, key) { - //check for null - val = val ? val : ""; - //need to detect if the val is a string or an object - if (!angular.isString(val)) { - //if it's not a string we'll send it through the json serializer - var json = angular.toJson(val); - //then we need to url encode it so that it's safe - val = encodeURIComponent(json); - } - else { - //we still need to encode the string, it could contain line breaks, etc... - val = encodeURIComponent(val); - } - - query += "¯oParams[" + counter + "].key=" + key + "¯oParams[" + counter + "].value=" + val; - counter++; - }); - } - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "macroApiBaseUrl", - "GetMacroResultAsHtmlForEditor", - query)), - 'Failed to retrieve macro result for macro with alias ' + macroAlias); + $http.post( + umbRequestHelper.getApiUrl( + "macroApiBaseUrl", + "GetMacroResultAsHtmlForEditor"), { + macroAlias: macroAlias, + pageId: pageId, + macroParams: macroParamDictionary + }), + 'Failed to retrieve macro result for macro with alias ' + macroAlias); } - }; } 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 a8eda14161..4b56e55801 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 @@ -4,13 +4,13 @@ * @description Loads in data for media **/ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { - + /** internal method process the saving of data and post processing the result */ function saveMediaItem(content, action, files) { return umbRequestHelper.postSaveContent({ restApiUrl: umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "PostSave"), + "mediaApiBaseUrl", + "PostSave"), content: content, action: action, files: files, @@ -21,29 +21,38 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { } return { - + + getRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRecycleBin")), + 'Failed to retrieve data for media recycle bin'); + }, + /** - * @ngdoc method - * @name umbraco.resources.mediaResource#sort - * @methodOf umbraco.resources.mediaResource - * - * @description - * Sorts all children below a given parent node id, based on a collection of node-ids - * - * ##usage - *
      -         * var ids = [123,34533,2334,23434];
      -         * mediaResource.sort({ sortedIds: ids })
      -         *    .then(function() {
      -         *        $scope.complete = true;
      -         *    });
      -         * 
      - * @param {Object} args arguments object - * @param {Int} args.parentId the ID of the parent node - * @param {Array} options.sortedIds array of node IDs as they should be sorted - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#sort + * @methodOf umbraco.resources.mediaResource + * + * @description + * Sorts all children below a given parent node id, based on a collection of node-ids + * + * ##usage + *
      +          * var ids = [123,34533,2334,23434];
      +          * mediaResource.sort({ sortedIds: ids })
      +          *    .then(function() {
      +          *        $scope.complete = true;
      +          *    });
      +          * 
      + * @param {Object} args arguments object + * @param {Int} args.parentId the ID of the parent node + * @param {Array} options.sortedIds array of node IDs as they should be sorted + * @returns {Promise} resourcePromise object. + * + */ sort: function (args) { if (!args) { throw "args cannot be null"; @@ -56,37 +65,37 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), - { - parentId: args.parentId, - idSortOrder: args.sortedIds - }), - 'Failed to sort media'); + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostSort"), + { + parentId: args.parentId, + idSortOrder: args.sortedIds + }), + 'Failed to sort media'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#move - * @methodOf umbraco.resources.mediaResource - * - * @description - * Moves a node underneath a new parentId - * - * ##usage - *
      -         * mediaResource.move({ parentId: 1244, id: 123 })
      -         *    .then(function() {
      -         *        alert("node was moved");
      -         *    }, function(err){
      -         *      alert("node didnt move:" + err.data.Message); 
      -         *    });
      -         * 
      - * @param {Object} args arguments object - * @param {Int} args.idd the ID of the node to move - * @param {Int} args.parentId the ID of the parent node to move to - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#move + * @methodOf umbraco.resources.mediaResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
      +          * mediaResource.move({ parentId: 1244, id: 123 })
      +          *    .then(function() {
      +          *        alert("node was moved");
      +          *    }, function(err){
      +          *      alert("node didnt move:" + err.data.Message); 
      +          *    });
      +          * 
      + * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ move: function (args) { if (!args) { throw "args cannot be null"; @@ -99,196 +108,196 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { } return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), - { - parentId: args.parentId, - id: args.id - }), - 'Failed to move media'); + $http.post(umbRequestHelper.getApiUrl("mediaApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move media'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets a media item with a given id - * - * ##usage - *
      -         * mediaResource.getById(1234)
      -         *    .then(function(media) {
      -         *        var myMedia = media; 
      -         *        alert('its here!');
      -         *    });
      -         * 
      - * - * @param {Int} id id of media item to return - * @returns {Promise} resourcePromise object containing the media item. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets a media item with a given id + * + * ##usage + *
      +          * mediaResource.getById(1234)
      +          *    .then(function(media) {
      +          *        var myMedia = media; 
      +          *        alert('its here!');
      +          *    });
      +          * 
      + * + * @param {Int} id id of media item to return + * @returns {Promise} resourcePromise object containing the media item. + * + */ getById: function (id) { - + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetById", - [{ id: id }])), - 'Failed to retrieve data for media id ' + id); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve data for media id ' + id); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#deleteById - * @methodOf umbraco.resources.mediaResource - * - * @description - * Deletes a media item with a given id - * - * ##usage - *
      -         * mediaResource.deleteById(1234)
      -         *    .then(function() {
      -         *        alert('its gone!');
      -         *    });
      -         * 
      - * - * @param {Int} id id of media item to delete - * @returns {Promise} resourcePromise object. - * - */ - deleteById: function(id) { + * @ngdoc method + * @name umbraco.resources.mediaResource#deleteById + * @methodOf umbraco.resources.mediaResource + * + * @description + * Deletes a media item with a given id + * + * ##usage + *
      +          * mediaResource.deleteById(1234)
      +          *    .then(function() {
      +          *        alert('its gone!');
      +          *    });
      +          * 
      + * + * @param {Int} id id of media item to delete + * @returns {Promise} resourcePromise object. + * + */ + deleteById: function (id) { return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "DeleteById", - [{ id: id }])), - 'Failed to delete item ' + id); + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete item ' + id); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getByIds - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets an array of media items, given a collection of ids - * - * ##usage - *
      -         * mediaResource.getByIds( [1234,2526,28262])
      -         *    .then(function(mediaArray) {
      -         *        var myDoc = contentArray; 
      -         *        alert('they are here!');
      -         *    });
      -         * 
      - * - * @param {Array} ids ids of media items to return as an array - * @returns {Promise} resourcePromise object containing the media items array. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getByIds + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets an array of media items, given a collection of ids + * + * ##usage + *
      +          * mediaResource.getByIds( [1234,2526,28262])
      +          *    .then(function(mediaArray) {
      +          *        var myDoc = contentArray; 
      +          *        alert('they are here!');
      +          *    });
      +          * 
      + * + * @param {Array} ids ids of media items to return as an array + * @returns {Promise} resourcePromise object containing the media items array. + * + */ getByIds: function (ids) { - + var idQuery = ""; - _.each(ids, function(item) { + _.each(ids, function (item) { idQuery += "ids=" + item + "&"; }); return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetByIds", - idQuery)), - 'Failed to retrieve data for media ids ' + ids); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetByIds", + idQuery)), + 'Failed to retrieve data for media ids ' + ids); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getScaffold - * @methodOf umbraco.resources.mediaResource - * - * @description - * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. - * - * - Parent Id must be provided so umbraco knows where to store the media - * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold - * - * The scaffold is used to build editors for media that has not yet been populated with data. - * - * ##usage - *
      -         * mediaResource.getScaffold(1234, 'folder')
      -         *    .then(function(scaffold) {
      -         *        var myDoc = scaffold;
      -         *        myDoc.name = "My new media item"; 
      -         *
      -         *        mediaResource.save(myDoc, true)
      -         *            .then(function(media){
      -         *                alert("Retrieved, updated and saved again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Int} parentId id of media item to return - * @param {String} alias mediatype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the media scaffold. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getScaffold + * @methodOf umbraco.resources.mediaResource + * + * @description + * Returns a scaffold of an empty media item, given the id of the media item to place it underneath and the media type alias. + * + * - Parent Id must be provided so umbraco knows where to store the media + * - Media Type alias must be provided so umbraco knows which properties to put on the media scaffold + * + * The scaffold is used to build editors for media that has not yet been populated with data. + * + * ##usage + *
      +          * mediaResource.getScaffold(1234, 'folder')
      +          *    .then(function(scaffold) {
      +          *        var myDoc = scaffold;
      +          *        myDoc.name = "My new media item"; 
      +          *
      +          *        mediaResource.save(myDoc, true)
      +          *            .then(function(media){
      +          *                alert("Retrieved, updated and saved again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Int} parentId id of media item to return + * @param {String} alias mediatype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the media scaffold. + * + */ getScaffold: function (parentId, alias) { - + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), - 'Failed to retrieve data for empty media item type ' + alias); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetEmpty", + [{ contentTypeAlias: alias }, { parentId: parentId }])), + 'Failed to retrieve data for empty media item type ' + alias); }, rootMedia: function () { - + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetRootMedia")), - 'Failed to retrieve data for root media'); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetRootMedia")), + 'Failed to retrieve data for root media'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#getChildren - * @methodOf umbraco.resources.mediaResource - * - * @description - * Gets children of a media item with a given id - * - * ##usage - *
      -         * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
      -         *    .then(function(contentArray) {
      -         *        var children = contentArray; 
      -         *        alert('they are here!');
      -         *    });
      -         * 
      - * - * @param {Int} parentid id of content item to return children of - * @param {Object} options optional options object - * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 - * @param {Int} options.pageNumber if paging data, current page index, default = 0 - * @param {String} options.filter if provided, query will only return those with names matching the filter - * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` - * @param {String} options.orderBy property to order items by, default: `SortOrder` - * @returns {Promise} resourcePromise object containing an array of content items. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildren + * @methodOf umbraco.resources.mediaResource + * + * @description + * Gets children of a media item with a given id + * + * ##usage + *
      +          * mediaResource.getChildren(1234, {pageSize: 10, pageNumber: 2})
      +          *    .then(function(contentArray) {
      +          *        var children = contentArray; 
      +          *        alert('they are here!');
      +          *    });
      +          * 
      + * + * @param {Int} parentid id of content item to return children of + * @param {Object} options optional options object + * @param {Int} options.pageSize if paging data, number of nodes per page, default = 0 + * @param {Int} options.pageNumber if paging data, current page index, default = 0 + * @param {String} options.filter if provided, query will only return those with names matching the filter + * @param {String} options.orderDirection can be `Ascending` or `Descending` - Default: `Ascending` + * @param {String} options.orderBy property to order items by, default: `SortOrder` + * @returns {Promise} resourcePromise object containing an array of content items. + * + */ getChildren: function (parentId, options) { var defaults = { @@ -296,7 +305,8 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { pageNumber: 0, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + orderBySystemField: true }; if (options === undefined) { options = {}; @@ -313,112 +323,165 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { options.orderDirection = "Descending"; } + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetChildren", - [ - { id: parentId }, - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { filter: options.filter } - ])), - 'Failed to retrieve children for media item ' + parentId); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildren", + [ + { id: parentId }, + { pageNumber: options.pageNumber }, + { pageSize: options.pageSize }, + { orderBy: options.orderBy }, + { orderDirection: options.orderDirection }, + { orderBySystemField: toBool(options.orderBySystemField) }, + { filter: options.filter } + ])), + 'Failed to retrieve children for media item ' + parentId); }, - + /** - * @ngdoc method - * @name umbraco.resources.mediaResource#save - * @methodOf umbraco.resources.mediaResource - * - * @description - * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation - * if the media item needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
      -         * mediaResource.getById(1234)
      -         *    .then(function(media) {
      -         *          media.name = "I want a new name!";
      -         *          mediaResource.save(media, false)
      -         *            .then(function(media){
      -         *                alert("Retrieved, updated and saved again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Object} media The media item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ + * @ngdoc method + * @name umbraco.resources.mediaResource#save + * @methodOf umbraco.resources.mediaResource + * + * @description + * Saves changes made to a media item, if the media item is new, the isNew paramater must be passed to force creation + * if the media item needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
      +          * mediaResource.getById(1234)
      +          *    .then(function(media) {
      +          *          media.name = "I want a new name!";
      +          *          mediaResource.save(media, false)
      +          *            .then(function(media){
      +          *                alert("Retrieved, updated and saved again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Object} media The media item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ save: function (media, isNew, files) { return saveMediaItem(media, "save" + (isNew ? "New" : ""), files); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#addFolder - * @methodOf umbraco.resources.mediaResource - * - * @description - * Shorthand for adding a media item of the type "Folder" under a given parent ID - * - * ##usage - *
      -         * mediaResource.addFolder("My gallery", 1234)
      -         *    .then(function(folder) {
      -         *        alert('New folder');
      -         *    });
      -         * 
      - * - * @param {string} name Name of the folder to create - * @param {int} parentId Id of the media item to create the folder underneath - * @returns {Promise} resourcePromise object. - * - */ - addFolder: function(name, parentId){ + * @ngdoc method + * @name umbraco.resources.mediaResource#addFolder + * @methodOf umbraco.resources.mediaResource + * + * @description + * Shorthand for adding a media item of the type "Folder" under a given parent ID + * + * ##usage + *
      +          * mediaResource.addFolder("My gallery", 1234)
      +          *    .then(function(folder) {
      +          *        alert('New folder');
      +          *    });
      +          * 
      + * + * @param {string} name Name of the folder to create + * @param {int} parentId Id of the media item to create the folder underneath + * @returns {Promise} resourcePromise object. + * + */ + addFolder: function (name, parentId) { return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper - .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), - { - name: name, - parentId: parentId - }), - 'Failed to add folder'); + $http.post(umbRequestHelper + .getApiUrl("mediaApiBaseUrl", "PostAddFolder"), + { + name: name, + parentId: parentId + }), + 'Failed to add folder'); }, /** - * @ngdoc method - * @name umbraco.resources.mediaResource#emptyRecycleBin - * @methodOf umbraco.resources.mediaResource - * - * @description - * Empties the media recycle bin - * - * ##usage - *
      -         * mediaResource.emptyRecycleBin()
      -         *    .then(function() {
      -         *        alert('its empty!');
      -         *    });
      -         * 
      - * - * @returns {Promise} resourcePromise object. - * - */ - emptyRecycleBin: function() { + * @ngdoc method + * @name umbraco.resources.mediaResource#getChildFolders + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves all media children with types used as folders. + * Uses the convention of looking for media items with mediaTypes ending in + * *Folder so will match "Folder", "bannerFolder", "secureFolder" etc, + * + * ##usage + *
      +          * mediaResource.getChildFolders(1234)
      +          *    .then(function(data) {
      +          *        alert('folders');
      +          *    });
      +          * 
      + * + * @param {int} parentId Id of the media item to query for child folders + * @returns {Promise} resourcePromise object. + * + */ + getChildFolders: function (parentId) { + if (!parentId) { + parentId = -1; + } + return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "EmptyRecycleBin")), - 'Failed to empty the recycle bin'); + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetChildFolders", + [ + { id: parentId } + ])), + 'Failed to retrieve child folders for media item ' + parentId); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#emptyRecycleBin + * @methodOf umbraco.resources.mediaResource + * + * @description + * Empties the media recycle bin + * + * ##usage + *
      +          * mediaResource.emptyRecycleBin()
      +          *    .then(function() {
      +          *        alert('its empty!');
      +          *    });
      +          * 
      + * + * @returns {Promise} resourcePromise object. + * + */ + emptyRecycleBin: function () { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "EmptyRecycleBin")), + 'Failed to empty the recycle bin'); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index 94699d4195..117edef77f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -3,10 +3,42 @@ * @name umbraco.resources.mediaTypeResource * @description Loads in data for media types **/ -function mediaTypeResource($q, $http, umbRequestHelper) { +function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { + getCount: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "GetCount")), + 'Failed to retrieve count'); + }, + + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + + var query = { + contentTypeId: contentTypeId, + filterContentTypes: filterContentTypes, + filterPropertyTypes: filterPropertyTypes + }; + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "GetAvailableCompositeMediaTypes"), + query), + 'Failed to retrieve data for content type id ' + contentTypeId); + }, + /** * @ngdoc method * @name umbraco.resources.mediaTypeResource#getAllowedTypes @@ -21,7 +53,7 @@ function mediaTypeResource($q, $http, umbRequestHelper) { * .then(function(array) { * $scope.type = type; * }); - * + * * @param {Int} mediaId id of the media item to retrive allowed child types for * @returns {Promise} resourcePromise object. * @@ -35,6 +67,142 @@ function mediaTypeResource($q, $http, umbRequestHelper) { "GetAllowedChildren", [{ contentId: mediaId }])), 'Failed to retrieve allowed types for media id ' + mediaId); + }, + + getById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve content type'); + }, + + getAll: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "GetAll")), + 'Failed to retrieve all content types'); + }, + + getScaffold: function (parentId) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "GetEmpty", { parentId: parentId })), + 'Failed to retrieve content type scaffold'); + }, + + deleteById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to retrieve content type'); + }, + + deleteContainerById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "DeleteContainer", + [{ id: id }])), + 'Failed to delete content type contaier'); + }, + + save: function (contentType) { + + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostSave"), saveModel), + 'Failed to save data for content type id ' + contentType.id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.mediaTypeResource#move + * @methodOf umbraco.resources.mediaTypeResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
      +         * mediaTypeResource.move({ parentId: 1244, id: 123 })
      +         *    .then(function() {
      +         *        alert("node was moved");
      +         *    }, function(err){
      +         *      alert("node didnt move:" + err.data.Message);
      +         *    });
      +         * 
      + * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, + + copy: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to copy content'); + }, + + createContainer: function(parentId, name) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "mediaTypeApiBaseUrl", + "PostCreateContainer", + { parentId: parentId, name: name })), + 'Failed to create a folder under parent id ' + parentId); } }; 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 42db4f6366..2073307db9 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 @@ -4,25 +4,25 @@ * @description Loads in data for members **/ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { - + /** internal method process the saving of data and post processing the result */ function saveMember(content, action, files) { - + return umbRequestHelper.postSaveContent({ restApiUrl: umbRequestHelper.getApiUrl( - "memberApiBaseUrl", - "PostSave"), + "memberApiBaseUrl", + "PostSave"), content: content, action: action, - files: files, - dataFormatter: function(c, a) { + files: files, + dataFormatter: function (c, a) { return umbDataFormatter.formatMemberPostData(c, a); } }); } return { - + getPagedResults: function (memberTypeAlias, options) { if (memberTypeAlias === 'all-members') { @@ -34,7 +34,8 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { pageNumber: 1, filter: '', orderDirection: "Ascending", - orderBy: "LoginName" + orderBy: "LoginName", + orderBySystemField: true }; if (options === undefined) { options = {}; @@ -51,179 +52,194 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { options.orderDirection = "Descending"; } + //converts the value to a js bool + function toBool(v) { + if (angular.isNumber(v)) { + return v > 0; + } + if (angular.isString(v)) { + return v === "true"; + } + if (typeof v === "boolean") { + return v; + } + return false; + } + var params = [ - { pageNumber: options.pageNumber }, - { pageSize: options.pageSize }, - { orderBy: options.orderBy }, - { orderDirection: options.orderDirection }, - { 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) { 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); }, /** - * @ngdoc method - * @name umbraco.resources.memberResource#getByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Gets a member item with a given key - * - * ##usage - *
      -         * memberResource.getByKey("0000-0000-000-00000-000")
      -         *    .then(function(member) {
      -         *        var mymember = member; 
      -         *        alert('its here!');
      -         *    });
      -         * 
      - * - * @param {Guid} key key of member item to return - * @returns {Promise} resourcePromise object containing the member item. - * - */ + * @ngdoc method + * @name umbraco.resources.memberResource#getByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Gets a member item with a given key + * + * ##usage + *
      +          * memberResource.getByKey("0000-0000-000-00000-000")
      +          *    .then(function(member) {
      +          *        var mymember = member; 
      +          *        alert('its here!');
      +          *    });
      +          * 
      + * + * @param {Guid} key key of member item to return + * @returns {Promise} resourcePromise object containing the member item. + * + */ 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); }, /** - * @ngdoc method - * @name umbraco.resources.memberResource#deleteByKey - * @methodOf umbraco.resources.memberResource - * - * @description - * Deletes a member item with a given key - * - * ##usage - *
      -         * memberResource.deleteByKey("0000-0000-000-00000-000")
      -         *    .then(function() {
      -         *        alert('its gone!');
      -         *    });
      -         * 
      - * - * @param {Guid} key id of member item to delete - * @returns {Promise} resourcePromise object. - * - */ + * @ngdoc method + * @name umbraco.resources.memberResource#deleteByKey + * @methodOf umbraco.resources.memberResource + * + * @description + * Deletes a member item with a given key + * + * ##usage + *
      +          * memberResource.deleteByKey("0000-0000-000-00000-000")
      +          *    .then(function() {
      +          *        alert('its gone!');
      +          *    });
      +          * 
      + * + * @param {Guid} key id of member item to delete + * @returns {Promise} resourcePromise object. + * + */ 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); }, /** - * @ngdoc method - * @name umbraco.resources.memberResource#getScaffold - * @methodOf umbraco.resources.memberResource - * - * @description - * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. - * - * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold - * - * The scaffold is used to build editors for member that has not yet been populated with data. - * - * ##usage - *
      -         * memberResource.getScaffold('client')
      -         *    .then(function(scaffold) {
      -         *        var myDoc = scaffold;
      -         *        myDoc.name = "My new member item"; 
      -         *
      -         *        memberResource.save(myDoc, true)
      -         *            .then(function(member){
      -         *                alert("Retrieved, updated and saved again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {String} alias membertype alias to base the scaffold on - * @returns {Promise} resourcePromise object containing the member scaffold. - * - */ + * @ngdoc method + * @name umbraco.resources.memberResource#getScaffold + * @methodOf umbraco.resources.memberResource + * + * @description + * Returns a scaffold of an empty member item, given the id of the member item to place it underneath and the member type alias. + * + * - Member Type alias must be provided so umbraco knows which properties to put on the member scaffold + * + * The scaffold is used to build editors for member that has not yet been populated with data. + * + * ##usage + *
      +          * memberResource.getScaffold('client')
      +          *    .then(function(scaffold) {
      +          *        var myDoc = scaffold;
      +          *        myDoc.name = "My new member item"; 
      +          *
      +          *        memberResource.save(myDoc, true)
      +          *            .then(function(member){
      +          *                alert("Retrieved, updated and saved again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {String} alias membertype alias to base the scaffold on + * @returns {Promise} resourcePromise object containing the member scaffold. + * + */ 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); } }, - + /** - * @ngdoc method - * @name umbraco.resources.memberResource#save - * @methodOf umbraco.resources.memberResource - * - * @description - * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation - * if the member needs to have files attached, they must be provided as the files param and passed separately - * - * - * ##usage - *
      -         * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
      -         *    .then(function(member) {
      -         *          member.name = "Bob";
      -         *          memberResource.save(member, false)
      -         *            .then(function(member){
      -         *                alert("Retrieved, updated and saved again");
      -         *            });
      -         *    });
      -         * 
      - * - * @param {Object} media The member item object with changes applied - * @param {Bool} isNew set to true to create a new item or to update an existing - * @param {Array} files collection of files for the media item - * @returns {Promise} resourcePromise object containing the saved media item. - * - */ + * @ngdoc method + * @name umbraco.resources.memberResource#save + * @methodOf umbraco.resources.memberResource + * + * @description + * Saves changes made to a member, if the member is new, the isNew paramater must be passed to force creation + * if the member needs to have files attached, they must be provided as the files param and passed separately + * + * + * ##usage + *
      +          * memberResource.getBykey("23234-sd8djsd-3h8d3j-sdh8d")
      +          *    .then(function(member) {
      +          *          member.name = "Bob";
      +          *          memberResource.save(member, false)
      +          *            .then(function(member){
      +          *                alert("Retrieved, updated and saved again");
      +          *            });
      +          *    });
      +          * 
      + * + * @param {Object} media The member item object with changes applied + * @param {Bool} isNew set to true to create a new item or to update an existing + * @param {Array} files collection of files for the media item + * @returns {Promise} resourcePromise object containing the saved media item. + * + */ save: function (member, isNew, files) { return saveMember(member, "save" + (isNew ? "New" : ""), files); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index 587ee4c58b..0649277c54 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -3,10 +3,44 @@ * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ -function memberTypeResource($q, $http, umbRequestHelper) { +function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberTypeApiBaseUrl", + "GetAvailableCompositeMemberTypes", + query)), + 'Failed to retrieve data for content type id ' + contentTypeId); + }, + //return all member types getTypes: function () { @@ -16,6 +50,59 @@ function memberTypeResource($q, $http, umbRequestHelper) { "memberTypeApiBaseUrl", "GetAllTypes")), 'Failed to retrieve data for member types id'); + }, + + getById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberTypeApiBaseUrl", + "GetById", + [{ id: id }])), + 'Failed to retrieve content type'); + }, + + deleteById: function (id) { + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "memberTypeApiBaseUrl", + "DeleteById", + [{ id: id }])), + 'Failed to delete member type'); + }, + + getScaffold: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "memberTypeApiBaseUrl", + "GetEmpty")), + 'Failed to retrieve content type scaffold'); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#save + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Saves or update a member type + * + * @param {Object} content data type object to create/update + * @returns {Promise} resourcePromise object. + * + */ + save: function (contentType) { + + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), saveModel), + 'Failed to save data for member type id ' + contentType.id); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js index ea4bee718f..73b6394b0a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/tree.resource.js @@ -16,7 +16,7 @@ function treeResource($q, $http, umbRequestHelper) { /** internal method to get the tree menu url */ function getTreeMenuUrl(node) { if (!node.menuUrl) { - throw "No menuUrl property found on the tree node, cannot load menu"; + return null; } return node.menuUrl; } @@ -26,10 +26,16 @@ function treeResource($q, $http, umbRequestHelper) { /** Loads in the data to display the nodes menu */ loadMenu: function (node) { - - return umbRequestHelper.resourcePromise( - $http.get(getTreeMenuUrl(node)), - "Failed to retrieve data for a node's menu " + node.id); + var treeMenuUrl = getTreeMenuUrl(node); + if (treeMenuUrl !== undefined && treeMenuUrl !== null && treeMenuUrl.length > 0) { + return umbRequestHelper.resourcePromise( + $http.get(getTreeMenuUrl(node)), + "Failed to retrieve data for a node's menu " + node.id); + } else { + return $q.reject({ + errorMsg: "No tree menu url defined for node " + node.id + }); + } }, /** Loads in the data to display the nodes for an application */ diff --git a/src/Umbraco.Web.UI.Client/src/common/security/interceptor.js b/src/Umbraco.Web.UI.Client/src/common/security/interceptor.js index 56e0eb3041..2b757707ba 100644 --- a/src/Umbraco.Web.UI.Client/src/common/security/interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/security/interceptor.js @@ -1,6 +1,6 @@ angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']) // This http interceptor listens for authentication successes and failures - .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', function ($injector, queue, notifications) { + .factory('securityInterceptor', ['$injector', 'securityRetryQueue', 'notificationsService', 'requestInterceptorFilter', function ($injector, queue, notifications, requestInterceptorFilter) { return function(promise) { return promise.then( @@ -19,6 +19,19 @@ angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']) return promise; }, function(originalResponse) { // Intercept failed requests + + //Here we'll check if we should ignore the error, this will be based on an original header set + var headers = originalResponse.config ? originalResponse.config.headers : {}; + if (headers["x-umb-ignore-error"] === "ignore") { + //exit/ignore + return promise; + } + var filtered = _.find(requestInterceptorFilter(), function(val) { + return originalResponse.config.url.indexOf(val) > 0; + }); + if (filtered) { + return promise; + } //A 401 means that the user is not logged in if (originalResponse.status === 401) { @@ -72,6 +85,10 @@ angular.module('umbraco.security.interceptor', ['umbraco.security.retryQueue']) }; }]) + .value('requestInterceptorFilter', function() { + return ["www.gravatar.com"]; + }) + // We have to add the interceptor to the queue as a string because the interceptor depends upon service instances that are not available in the config block. .config(['$httpProvider', function ($httpProvider) { $httpProvider.responseInterceptors.push('securityInterceptor'); 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 4576f3344b..b1464e5c9d 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 @@ -2,13 +2,32 @@ /** * @ngdoc service * @name umbraco.services.contentEditingHelper -* @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by +* @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, keyboardService) { +function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) { + + function isValidIdentifier(id){ + //empty id <= 0 + if(angular.isNumber(id) && id > 0){ + return true; + } + + //empty guid + if(id === "00000000-0000-0000-0000-000000000000"){ + return false; + } + + //empty string / alias + if(id === ""){ + return false; + } + + return true; + } return { - + /** Used by the content editor and mini content editor to perform saving operations */ contentEditorPerformSave: function (args) { if (!angular.isObject(args)) { @@ -27,11 +46,16 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica throw "args.saveMethod is not defined"; } + var redirectOnFailure = args.redirectOnFailure !== undefined ? args.redirectOnFailure : true; + var self = this; + //we will use the default one for content if not specified + var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; + var deferred = $q.defer(); - - if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage })) { + + if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage, action: args.action })) { args.scope.busy = true; @@ -43,7 +67,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica self.handleSuccessfulSave({ scope: args.scope, savedContent: data, - rebindCallback: self.reBindChangedProperties(args.content, data) + rebindCallback: function() { + rebindCallback.apply(self, [args.content, data]); + } }); args.scope.busy = false; @@ -51,9 +77,11 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica }, function (err) { self.handleSaveError({ - redirectOnFailure: true, + redirectOnFailure: redirectOnFailure, err: err, - rebindCallback: self.reBindChangedProperties(args.content, err.data) + rebindCallback: function() { + rebindCallback.apply(self, [args.content, err.data]); + } }); //show any notifications if (angular.isArray(err.data.notifications)) { @@ -71,9 +99,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica return deferred.promise; }, - + + /** 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 + The content.allowedActions parameter contains a list of chars, each represents a button by permission so here we'll build the buttons according to the chars of the user. */ configureContentEditorButtons: function (args) { @@ -99,44 +128,42 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica switch (ch) { case "U": //publish action - keyboardService.bind("ctrl+p", args.methods.saveAndPublish); - return { letter: ch, labelKey: "buttons_saveAndPublish", handler: args.methods.saveAndPublish, - hotKey: "ctrl+p" + hotKey: "ctrl+p", + hotKeyWhenHidden: true }; case "H": //send to publish - keyboardService.bind("ctrl+p", args.methods.sendToPublish); - return { letter: ch, labelKey: "buttons_saveToPublish", handler: args.methods.sendToPublish, - hotKey: "ctrl+p" + hotKey: "ctrl+p", + hotKeyWhenHidden: true }; case "A": //save - keyboardService.bind("ctrl+s", args.methods.save); return { letter: ch, labelKey: "buttons_save", handler: args.methods.save, - hotKey: "ctrl+s" + hotKey: "ctrl+s", + hotKeyWhenHidden: true }; case "Z": //unpublish - keyboardService.bind("ctrl+u", args.methods.unPublish); - return { letter: ch, labelKey: "content_unPublish", - handler: args.methods.unPublish + handler: args.methods.unPublish, + hotKey: "ctrl+u", + hotKeyWhenHidden: true }; default: - return null; + return null; } } @@ -160,8 +187,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //Now we need to make the drop down button list, this is also slightly tricky because: //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. if (buttons.defaultButton) { //get the last index of the button order @@ -174,7 +201,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } - //if we are not creating, then we should add unpublish too, + //if we are not creating, then we should add unpublish too, // so long as it's already published and if the user has access to publish if (!args.create) { if (args.content.publishDate && _.contains(args.content.allowedActions, "U")) { @@ -235,13 +262,13 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } } - + actions.push(defaultAction); //Now we need to make the drop down button list, this is also slightly tricky because: //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. if (defaultAction) { //get the last index of the button order var lastIndex = _.indexOf(actionOrder, defaultAction); @@ -253,7 +280,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } - //if we are not creating, then we should add unpublish too, + //if we are not creating, then we should add unpublish too, // so long as it's already published and if the user has access to publish if (!creating) { if (content.publishDate && _.contains(content.allowedActions,"U")) { @@ -329,7 +356,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var allOrigProps = this.getAllProps(origContent); var allNewProps = this.getAllProps(savedContent); - function getNewProp(alias) { + function getNewProp(alias) { return _.find(allNewProps, function (item) { return item.alias === alias; }); @@ -343,12 +370,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica }; //check for changed built-in properties of the content for (var o in origContent) { - + //ignore the ones listed in the array if (shouldIgnore(o)) { continue; } - + if (!_.isEqual(origContent[o], savedContent[o])) { origContent[o] = savedContent[o]; } @@ -362,8 +389,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //they have changed so set the origContent prop to the new one var origVal = allOrigProps[p].value; allOrigProps[p].value = newProp.value; - - //instead of having a property editor $watch their expression to check if it has + + //instead of having a property editor $watch their expression to check if it has // been updated, instead we'll check for the existence of a special method on their model // and just call it. if (angular.isFunction(allOrigProps[p].onValueChanged)) { @@ -388,7 +415,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * A function to handle what happens when we have validation issues from the server side */ handleSaveError: function (args) { - + if (!args.err) { throw "args.err cannot be null"; } @@ -402,7 +429,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (args.err.status === 400) { //now we need to look through all the validation errors if (args.err.data && (args.err.data.ModelState)) { - + //wire up the server validation errs formHelper.handleServerValidation(args.err.data.ModelState); @@ -414,7 +441,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { args.rebindCallback(); } - + serverValidationManager.executeAndClearAllSubscriptions(); } @@ -453,7 +480,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) { - + //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. //call the callback @@ -471,14 +498,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * * @description * Changes the location to be editing the newly created content after create was successful. - * We need to decide if we need to redirect to edito mode or if we will remain in create mode. + * We need to decide if we need to redirect to edito mode or if we will remain in create mode. * We will only need to maintain create mode if we have not fulfilled the basic requirements for creating an entity which is at least having a name and ID */ redirectToCreatedContent: function (id, modelState) { //only continue if we are currently in create mode and if there is no 'Name' modelstate errors // since we need at least a name to create content. - if ($routeParams.create && (id > 0 && (!modelState || !modelState["Name"]))) { + if ($routeParams.create && (isValidIdentifier(id) && (!modelState || !modelState["Name"]))) { //need to change the location to not be in 'create' mode. Currently the route will be something like: // /belle/#/content/edit/1234?doctype=newsArticle&create=true @@ -487,7 +514,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //clear the query strings $location.search(""); - + //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); //don't add a browser history for this @@ -498,4 +525,4 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } }; } -angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); \ No newline at end of file +angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js new file mode 100644 index 0000000000..5c3e6eb4c8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -0,0 +1,350 @@ +/** + * @ngdoc service + * @name umbraco.services.contentTypeHelper + * @description A helper service for the content type editor + **/ +function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $injector, $q) { + + var contentTypeHelperService = { + + createIdArray: function(array) { + + var newArray = []; + + angular.forEach(array, function(arrayItem){ + + if(angular.isObject(arrayItem)) { + newArray.push(arrayItem.id); + } else { + newArray.push(arrayItem); + } + + }); + + return newArray; + + }, + + generateModels: function () { + var deferred = $q.defer(); + var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; + var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; + if (modelsBuilderEnabled && modelsResource) { + modelsResource.buildModels().then(function(result) { + deferred.resolve(result); + + //just calling this to get the servar back to life + modelsResource.getModelsOutOfDateStatus(); + + }, function(e) { + deferred.reject(e); + }); + } + else { + deferred.resolve(false); + } + return deferred.promise; + }, + + checkModelsBuilderStatus: function () { + var deferred = $q.defer(); + var modelsResource = $injector.has("modelsBuilderResource") ? $injector.get("modelsBuilderResource") : null; + var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); + + if (modelsBuilderEnabled && modelsResource) { + modelsResource.getModelsOutOfDateStatus().then(function(result) { + //Generate models buttons should be enabled if it is 0 + deferred.resolve(result.status === 0); + }); + } + else { + deferred.resolve(false); + } + return deferred.promise; + }, + + makeObjectArrayFromId: function (idArray, objectArray) { + var newArray = []; + + for (var idIndex = 0; idArray.length > idIndex; idIndex++) { + var id = idArray[idIndex]; + + for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { + var object = objectArray[objectIndex]; + if (id === object.id) { + newArray.push(object); + } + } + + } + + return newArray; + }, + + validateAddingComposition: function(contentType, compositeContentType) { + + //Validate that by adding this group that we are not adding duplicate property type aliases + + var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })); + var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + + var intersec = _.intersection(propertiesAdding, propAliasesExisting); + if (intersec.length > 0) { + //return the overlapping property aliases + return intersec; + } + + //no overlapping property aliases + return []; + }, + + mergeCompositeContentType: function(contentType, compositeContentType) { + + //Validate that there are no overlapping aliases + var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); + if (overlappingAliases.length > 0) { + throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); + } + + angular.forEach(compositeContentType.groups, function(compositionGroup) { + + // order composition groups based on sort order + compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); + + // get data type details + angular.forEach(compositionGroup.properties, function(property) { + dataTypeResource.getById(property.dataTypeId) + .then(function(dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + }); + + // set inherited state on tab + compositionGroup.inherited = true; + + // set inherited state on properties + angular.forEach(compositionGroup.properties, function(compositionProperty) { + compositionProperty.inherited = true; + }); + + // set tab state + compositionGroup.tabState = "inActive"; + + // if groups are named the same - merge the groups + angular.forEach(contentType.groups, function(contentTypeGroup) { + + if (contentTypeGroup.name === compositionGroup.name) { + + // set flag to show if properties has been merged into a tab + compositionGroup.groupIsMerged = true; + + // make group inherited + contentTypeGroup.inherited = true; + + // add properties to the top of the array + contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); + + // update sort order on all properties in merged group + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + + // make parentTabContentTypeNames to an array so we can push values + if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { + contentTypeGroup.parentTabContentTypeNames = []; + } + + // push name to array of merged composite content types + contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); + + // make parentTabContentTypes to an array so we can push values + if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { + contentTypeGroup.parentTabContentTypes = []; + } + + // push id to array of merged composite content types + contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); + + // get sort order from composition + contentTypeGroup.sortOrder = compositionGroup.sortOrder; + + // splice group to the top of the array + var contentTypeGroupCopy = angular.copy(contentTypeGroup); + var index = contentType.groups.indexOf(contentTypeGroup); + contentType.groups.splice(index, 1); + contentType.groups.unshift(contentTypeGroupCopy); + + } + + }); + + // if group is not merged - push it to the end of the array - before init tab + if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { + + // make parentTabContentTypeNames to an array so we can push values + if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { + compositionGroup.parentTabContentTypeNames = []; + } + + // push name to array of merged composite content types + compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); + + // make parentTabContentTypes to an array so we can push values + if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { + compositionGroup.parentTabContentTypes = []; + } + + // push id to array of merged composite content types + compositionGroup.parentTabContentTypes.push(compositeContentType.id); + + // push group before placeholder tab + contentType.groups.unshift(compositionGroup); + + } + + }); + + // sort all groups by sortOrder property + contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); + + return contentType; + + }, + + splitCompositeContentType: function (contentType, compositeContentType) { + + var groups = []; + + angular.forEach(contentType.groups, function(contentTypeGroup){ + + if( contentTypeGroup.tabState !== "init" ) { + + var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); + var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); + var groupIndex = contentType.groups.indexOf(contentTypeGroup); + + + if( idIndex !== -1 ) { + + var properties = []; + + // remove all properties from composite content type + angular.forEach(contentTypeGroup.properties, function(property){ + if(property.contentTypeId !== compositeContentType.id) { + properties.push(property); + } + }); + + // set new properties array to properties + contentTypeGroup.properties = properties; + + // remove composite content type name and id from inherited arrays + contentTypeGroup.parentTabContentTypes.splice(idIndex, 1); + contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1); + + // remove inherited state if there are no inherited properties + if(contentTypeGroup.parentTabContentTypes.length === 0) { + contentTypeGroup.inherited = false; + } + + // remove group if there are no properties left + if(contentTypeGroup.properties.length > 1) { + //contentType.groups.splice(groupIndex, 1); + groups.push(contentTypeGroup); + } + + } else { + groups.push(contentTypeGroup); + } + + } else { + groups.push(contentTypeGroup); + } + + // update sort order on properties + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + + }); + + contentType.groups = groups; + + }, + + updatePropertiesSortOrder: function (properties) { + + var sortOrder = 0; + + angular.forEach(properties, function(property) { + if( !property.inherited && property.propertyState !== "init") { + property.sortOrder = sortOrder; + } + sortOrder++; + }); + + return properties; + + }, + + getTemplatePlaceholder: function() { + + var templatePlaceholder = { + "name": "", + "icon": "icon-layout", + "alias": "templatePlaceholder", + "placeholder": true + }; + + return templatePlaceholder; + + }, + + insertDefaultTemplatePlaceholder: function(defaultTemplate) { + + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + + // add as default template + defaultTemplate = templatePlaceholder; + + return defaultTemplate; + + }, + + insertTemplatePlaceholder: function(array) { + + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + + // add as selected item + array.push(templatePlaceholder); + + return array; + + }, + + insertChildNodePlaceholder: function (array, name, icon, id) { + + var placeholder = { + "name": name, + "icon": icon, + "id": id + }; + + array.push(placeholder); + + } + + }; + + return contentTypeHelperService; +} +angular.module('umbraco.services').factory('contentTypeHelper', contentTypeHelper); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js new file mode 100644 index 0000000000..3cde632d4b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js @@ -0,0 +1,55 @@ +/** + * @ngdoc service + * @name umbraco.services.dataTypeHelper + * @description A helper service for data types + **/ +function dataTypeHelper() { + + var dataTypeHelperService = { + + createPreValueProps: function(preVals) { + + var preValues = []; + + for (var i = 0; i < preVals.length; i++) { + preValues.push({ + hideLabel: preVals[i].hideLabel, + alias: preVals[i].key, + description: preVals[i].description, + label: preVals[i].label, + view: preVals[i].view, + value: preVals[i].value + }); + } + + return preValues; + + }, + + rebindChangedProperties: function (origContent, savedContent) { + + //a method to ignore built-in prop changes + var shouldIgnore = function (propName) { + return _.some(["notifications", "ModelState"], function (i) { + return i === propName; + }); + }; + //check for changed built-in properties of the content + for (var o in origContent) { + + //ignore the ones listed in the array + if (shouldIgnore(o)) { + continue; + } + + if (!_.isEqual(origContent[o], savedContent[o])) { + origContent[o] = savedContent[o]; + } + } + } + + }; + + return dataTypeHelperService; +} +angular.module('umbraco.services').factory('dataTypeHelper', dataTypeHelper); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js index 00af2c57cb..263397787d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js @@ -2,13 +2,13 @@ * @ngdoc service * @name umbraco.services.dialogService * - * @requires $rootScope + * @requires $rootScope * @requires $compile * @requires $http * @requires $log * @requires $q * @requires $templateCache - * + * * @description * Application-wide service for handling modals, overlays and dialogs * By default it injects the passed template url into a div to body of the document @@ -22,10 +22,10 @@ *
        *    var dialog = dialogService.open({template: 'path/to/page.html', show: true, callback: done});
        *    functon done(data){
      - *      //The dialog has been submitted 
      + *      //The dialog has been submitted
        *      //data contains whatever the dialog has selected / attached
      - *    }     
      - * 
      + * } + * */ angular.module('umbraco.services') @@ -38,12 +38,12 @@ angular.module('umbraco.services') for (var i = 0; i < dialogs.length; i++) { var dialog = dialogs[i]; - //very special flag which means that global events cannot close this dialog - currently only used on the login + //very special flag which means that global events cannot close this dialog - currently only used on the login // dialog since it's special and cannot be closed without logging in. if (!dialog.manualClose) { dialog.close(args); } - + } } @@ -56,28 +56,18 @@ angular.module('umbraco.services') //this is not entirely enough since the damn webforms scriploader still complains if (dialog.iframe) { dialog.element.find("iframe").attr("src", "about:blank"); - $timeout(function () { - //we need to do more than just remove the element, this will not destroy the - // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont - // take care of this ourselves we have memory leaks. - dialog.element.remove(); - //SD: No idea why this is required but was there before - pretty sure it's not required - $("#" + dialog.element.attr("id")).remove(); - dialog.scope.$destroy(); - }, 1000); - } else { - //we need to do more than just remove the element, this will not destroy the - // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont - // take care of this ourselves we have memory leaks. - dialog.element.remove(); - //SD: No idea why this is required but was there before - pretty sure it's not required - $("#" + dialog.element.attr("id")).remove(); - dialog.scope.$destroy(); } - } - //remove 'this' dialog from the dialogs array - dialogs = _.reject(dialogs, function (i) { return i === dialog; }); + dialog.scope.$destroy(); + + //we need to do more than just remove the element, this will not destroy the + // scope in angular 1.1x, in angular 1.2x this is taken care of but if we dont + // take care of this ourselves we have memory leaks. + dialog.element.remove(); + + //remove 'this' dialog from the dialogs array + dialogs = _.reject(dialogs, function (i) { return i === dialog; }); + } } /** Internal method that handles opening all dialogs */ @@ -93,24 +83,24 @@ angular.module('umbraco.services') template: "views/common/notfound.html", callback: undefined, closeCallback: undefined, - element: undefined, + element: undefined, // It will set this value as a property on the dialog controller's scope as dialogData, - // used to pass in custom data to the dialog controller's $scope. Though this is near identical to - // the dialogOptions property that is also set the the dialog controller's $scope object. + // used to pass in custom data to the dialog controller's $scope. Though this is near identical to + // the dialogOptions property that is also set the the dialog controller's $scope object. // So there's basically 2 ways of doing the same thing which we're now stuck with and in fact // dialogData has another specially attached property called .selection which gets used. dialogData: undefined }; var dialog = angular.extend(defaults, options); - + //NOTE: People should NOT pass in a scope object that is legacy functoinality and causes problems. We will ALWAYS // destroy the scope when the dialog is closed regardless if it is in use elsewhere which is why it shouldn't be done. var scope = options.scope || $rootScope.$new(); - //Modal dom obj and unique id + //Modal dom obj and set id to old-dialog-service - used until we get all dialogs moved the the new overlay directive dialog.element = $('
      '); - var id = dialog.template.replace('.html', '').replace('.aspx', '').replace(/[\/|\.|:\&\?\=]/g, "-") + '-' + scope.$id; + var id = "old-dialog-service"; if (options.inline) { dialog.animation = ""; @@ -156,7 +146,7 @@ angular.module('umbraco.services') dialog.element.css("width", dialog.width); - //Autoshow + //Autoshow if (dialog.show) { dialog.element.modal('show'); } @@ -167,7 +157,7 @@ angular.module('umbraco.services') else { //We need to load the template with an httpget and once it's loaded we'll compile and assign the result to the container - // object. However since the result could be a promise or just data we need to use a $q.when. We still need to return the + // object. However since the result could be a promise or just data we need to use a $q.when. We still need to return the // $modal object so we'll actually return the modal object synchronously without waiting for the promise. Otherwise this openDialog // method will always need to return a promise which gets nasty because of promises in promises plus the result just needs a reference // to the $modal object which will not change (only it's contents will change). @@ -177,7 +167,7 @@ angular.module('umbraco.services') // Build modal object dialog.element.html(template); - //append to body or other container element + //append to body or other container element dialog.container.append(dialog.element); // Compile modal content @@ -224,8 +214,8 @@ angular.module('umbraco.services') scope.close = function (data) { dialog.close(data); }; - - //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow). + + //NOTE: This can ONLY ever be used to show the dialog if dialog.show is false (autoshow). // You CANNOT call show() after you call hide(). hide = close, they are the same thing and once // a dialog is closed it's resources are disposed of. scope.show = function () { @@ -237,7 +227,7 @@ angular.module('umbraco.services') //just show normally dialog.element.modal('show'); } - + }; scope.select = function (item) { @@ -266,11 +256,11 @@ angular.module('umbraco.services') dialog.scope = scope; - //Autoshow + //Autoshow if (dialog.show) { scope.show(); } - + }); //Return the modal object outside of the promise! @@ -368,7 +358,7 @@ angular.module('umbraco.services') * @param {Function} options.callback callback function * @returns {Object} modal object */ - contentPicker: function (options) { + contentPicker: function (options) { options.treeAlias = "content"; options.section = "content"; @@ -424,7 +414,7 @@ angular.module('umbraco.services') * @returns {Object} modal object */ memberPicker: function (options) { - + options.treeAlias = "member"; options.section = "member"; @@ -511,7 +501,7 @@ angular.module('umbraco.services') * @name umbraco.services.dialogService#embedDialog * @methodOf umbraco.services.dialogService * @description - * Opens a dialog to an embed dialog + * Opens a dialog to an embed dialog */ embedDialog: function (options) { options.template = 'views/common/dialogs/rteembed.html'; @@ -546,4 +536,4 @@ angular.module('umbraco.services') return openDialog(options); } }; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 057e0b8cff..9dfa440a0e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -7,7 +7,7 @@ * A utility class used to streamline how forms are developed, to ensure that validation is check and displayed consistently and to ensure that the correct events * fire when they need to. */ -function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService) { +function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService, localizationService) { return { /** @@ -50,7 +50,7 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati } //the first thing any form must do is broadcast the formSubmitting event - args.scope.$broadcast("formSubmitting", { scope: args.scope }); + args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); //then check if the form is valid if (!args.skipValidation) { @@ -157,9 +157,19 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati * * @param {object} err The error object returned from the http promise */ - handleServerValidation: function(modelState) { + handleServerValidation: function (modelState) { for (var e in modelState) { + //This is where things get interesting.... + // We need to support validation for all editor types such as both the content and content type editors. + // The Content editor ModelState is quite specific with the way that Properties are validated especially considering + // that each property is a User Developer property editor. + // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations + // system. + // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties, + // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect + // this, then we know it's a Property. + //the alias in model state can be in dot notation which indicates // * the first part is the content property alias // * the second part is the field to which the valiation msg is associated with @@ -167,7 +177,11 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati //If it is not prefixed with "Properties" that means the error is for a field of the object directly. var parts = e.split("."); - if (parts.length > 1) { + + //Check if this is for content properties - specific to content/media/member editors because those are special + // user defined properties with custom controls. + if (parts.length > 1 && parts[0] === "_Properties") { + var propertyAlias = parts[1]; //if it contains 2 '.' then we will wire it up to a property's field @@ -182,12 +196,15 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati } else { - //the parts are only 1, this means its not a property but a native content property - serverValidationManager.addFieldError(parts[0], modelState[e][0]); + + //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: + // Groups[0].Properties[2].Alias + serverValidationManager.addFieldError(e, modelState[e][0]); } //add to notifications notificationsService.error("Validation", modelState[e][0]); + } } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js index 6d83c1b907..04194838ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/iconhelper.service.js @@ -109,11 +109,16 @@ function iconHelper($q, $timeout) { }, formatContentTypeIcons: function (contentTypes) { for (var i = 0; i < contentTypes.length; i++) { - contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon); + if (!contentTypes[i].icon) { + //just to be safe (e.g. when focus was on close link and hitting save) + contentTypes[i].icon = "icon-document"; // default icon + } else { + contentTypes[i].icon = this.convertFromLegacyIcon(contentTypes[i].icon); + } //couldnt find replacement if(contentTypes[i].icon.indexOf(".") > 0){ - contentTypes[i].icon = "icon-document-dashed-line"; + contentTypes[i].icon = "icon-document-dashed-line"; } } return contentTypes; @@ -128,6 +133,10 @@ function iconHelper($q, $timeout) { }, /** If the icon is legacy */ isLegacyIcon: function (icon) { + if(!icon) { + return false; + } + if(icon.startsWith('..')){ return false; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js index 0a7be9f5a4..f66b0b0ed0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/keyboard.service.js @@ -1,273 +1,323 @@ // This service was based on OpenJS library available in BSD License // http://www.openjs.com/scripts/events/keyboard_shortcuts/index.php -angular.module('umbraco.services') -.factory('keyboardService', ['$window', '$timeout', function ($window, $timeout) { - var keyboardManagerService = {}; - var defaultOpt = { - 'type': 'keydown', - 'propagate': false, - 'inputDisabled': false, - 'target': $window.document, - 'keyCode': false - }; - - var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; - // Store all keyboard combination shortcuts - keyboardManagerService.keyboardEvent = {}; +function keyboardService($window, $timeout) { + + var keyboardManagerService = {}; + + var defaultOpt = { + 'type': 'keydown', + 'propagate': false, + 'inputDisabled': false, + 'target': $window.document, + 'keyCode': false + }; + // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken + var shift_nums = { + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + ";": ":", + "'": "\"", + ",": "<", + ".": ">", + "/": "?", + "\\": "|" + }; - // Add a new keyboard combination shortcut - keyboardManagerService.bind = function (label, callback, opt) { + // Special Keys - and their codes + var special_keys = { + 'esc': 27, + 'escape': 27, + 'tab': 9, + 'space': 32, + 'return': 13, + 'enter': 13, + 'backspace': 8, - //replace ctrl key with meta key - if(isMac && label !== "ctrl+space"){ - label = label.replace("ctrl","meta"); - } + 'scrolllock': 145, + 'scroll_lock': 145, + 'scroll': 145, + 'capslock': 20, + 'caps_lock': 20, + 'caps': 20, + 'numlock': 144, + 'num_lock': 144, + 'num': 144, - //always try to unbind first, so we dont have multiple actions on the same key - keyboardManagerService.unbind(label); + 'pause': 19, + 'break': 19, - var fct, elt, code, k; - // Initialize opt object - opt = angular.extend({}, defaultOpt, opt); - label = label.toLowerCase(); - elt = opt.target; - if(typeof opt.target === 'string'){ - elt = document.getElementById(opt.target); - } + 'insert': 45, + 'home': 36, + 'delete': 46, + 'end': 35, - - fct = function (e) { - e = e || $window.event; + 'pageup': 33, + 'page_up': 33, + 'pu': 33, - // Disable event handler when focus input and textarea - if (opt['inputDisabled']) { - var elt; - if (e.target){ - elt = e.target; - }else if (e.srcElement){ - elt = e.srcElement; - } + 'pagedown': 34, + 'page_down': 34, + 'pd': 34, - if (elt.nodeType === 3){elt = elt.parentNode;} - if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA'){return;} - } + 'left': 37, + 'up': 38, + 'right': 39, + 'down': 40, - // Find out which key is pressed - if (e.keyCode){ - code = e.keyCode; - }else if (e.which){ - code = e.which; - } + 'f1': 112, + 'f2': 113, + 'f3': 114, + 'f4': 115, + 'f5': 116, + 'f6': 117, + 'f7': 118, + 'f8': 119, + 'f9': 120, + 'f10': 121, + 'f11': 122, + 'f12': 123 + }; - var character = String.fromCharCode(code).toLowerCase(); + var isMac = navigator.platform.toUpperCase().indexOf('MAC')>=0; - if (code === 188){character = ",";} // If the user presses , when the type is onkeydown - if (code === 190){character = ".";} // If the user presses , when the type is onkeydown + // The event handler for bound element events + function eventHandler(e) { + e = e || $window.event; - var keys = label.split("+"); - // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked - var kp = 0; - // Work around for stupid Shift key bug created by using lowercase - as a result the shift+num combination was broken - var shift_nums = { - "`":"~", - "1":"!", - "2":"@", - "3":"#", - "4":"$", - "5":"%", - "6":"^", - "7":"&", - "8":"*", - "9":"(", - "0":")", - "-":"_", - "=":"+", - ";":":", - "'":"\"", - ",":"<", - ".":">", - "/":"?", - "\\":"|" - }; - // Special Keys - and their codes - var special_keys = { - 'esc':27, - 'escape':27, - 'tab':9, - 'space':32, - 'return':13, - 'enter':13, - 'backspace':8, + var code, k; - 'scrolllock':145, - 'scroll_lock':145, - 'scroll':145, - 'capslock':20, - 'caps_lock':20, - 'caps':20, - 'numlock':144, - 'num_lock':144, - 'num':144, + // Find out which key is pressed + if (e.keyCode) + { + code = e.keyCode; + } + else if (e.which) { + code = e.which; + } - 'pause':19, - 'break':19, + var character = String.fromCharCode(code).toLowerCase(); - 'insert':45, - 'home':36, - 'delete':46, - 'end':35, + if (code === 188){character = ",";} // If the user presses , when the type is onkeydown + if (code === 190){character = ".";} // If the user presses , when the type is onkeydown - 'pageup':33, - 'page_up':33, - 'pu':33, + var propagate = true; - 'pagedown':34, - 'page_down':34, - 'pd':34, + //Now we need to determine which shortcut this event is for, we'll do this by iterating over each + //registered shortcut to find the match. We use Find here so that the loop exits as soon + //as we've found the one we're looking for + _.find(_.keys(keyboardManagerService.keyboardEvent), function(key) { - 'left':37, - 'up':38, - 'right':39, - 'down':40, + var shortcutLabel = key; + var shortcutVal = keyboardManagerService.keyboardEvent[key]; - 'f1':112, - 'f2':113, - 'f3':114, - 'f4':115, - 'f5':116, - 'f6':117, - 'f7':118, - 'f8':119, - 'f9':120, - 'f10':121, - 'f11':122, - 'f12':123 - }; - // Some modifiers key - var modifiers = { - shift: { - wanted: false, - pressed: e.shiftKey ? true : false - }, - ctrl : { - wanted: false, - pressed: e.ctrlKey ? true : false - }, - alt : { - wanted: false, - pressed: e.altKey ? true : false - }, - meta : { //Meta is Mac specific - wanted: false, - pressed: e.metaKey ? true : false - } - }; - // Foreach keys in label (split on +) - var l = keys.length; - for (var i = 0; i < l; i++) { + // Key Pressed - counts the number of valid keypresses - if it is same as the number of keys, the shortcut function is invoked + var kp = 0; - var k=keys[i]; - switch (k) { - case 'ctrl': - case 'control': - kp++; - modifiers.ctrl.wanted = true; - break; - case 'shift': - case 'alt': - case 'meta': - kp++; - modifiers[k].wanted = true; - break; - } + // Some modifiers key + var modifiers = { + shift: { + wanted: false, + pressed: e.shiftKey ? true : false + }, + ctrl: { + wanted: false, + pressed: e.ctrlKey ? true : false + }, + alt: { + wanted: false, + pressed: e.altKey ? true : false + }, + meta: { //Meta is Mac specific + wanted: false, + pressed: e.metaKey ? true : false + } + }; - if (k.length > 1) { // If it is a special key - if(special_keys[k] === code){ - kp++; - } + var keys = shortcutLabel.split("+"); + var opt = shortcutVal.opt; + var callback = shortcutVal.callback; - } else if (opt['keyCode']) { // If a specific key is set into the config - if (opt['keyCode'] === code) { - kp++; - } + // Foreach keys in label (split on +) + var l = keys.length; + for (var i = 0; i < l; i++) { - } else { // The special keys did not match - if(character === k) { - kp++; - }else { - if(shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase - character = shift_nums[character]; - if(character === k){ - kp++; - } - } - } - } + var k = keys[i]; + switch (k) { + case 'ctrl': + case 'control': + kp++; + modifiers.ctrl.wanted = true; + break; + case 'shift': + case 'alt': + case 'meta': + kp++; + modifiers[k].wanted = true; + break; + } - } //for end + if (k.length > 1) { // If it is a special key + if (special_keys[k] === code) { + kp++; + } + } + else if (opt['keyCode']) { // If a specific key is set into the config + if (opt['keyCode'] === code) { + kp++; + } + } + else { // The special keys did not match + if (character === k) { + kp++; + } + else { + if (shift_nums[character] && e.shiftKey) { // Stupid Shift key bug created by using lowercase + character = shift_nums[character]; + if (character === k) { + kp++; + } + } + } + } - if(kp === keys.length && - modifiers.ctrl.pressed === modifiers.ctrl.wanted && - modifiers.shift.pressed === modifiers.shift.wanted && - modifiers.alt.pressed === modifiers.alt.wanted && - modifiers.meta.pressed === modifiers.meta.wanted) { - $timeout(function() { - callback(e); - }, 1); + } //for end - if(!opt['propagate']) { // Stop the event - // e.cancelBubble is supported by IE - this will kill the bubbling process. - e.cancelBubble = true; - e.returnValue = false; + if (kp === keys.length && + modifiers.ctrl.pressed === modifiers.ctrl.wanted && + modifiers.shift.pressed === modifiers.shift.wanted && + modifiers.alt.pressed === modifiers.alt.wanted && + modifiers.meta.pressed === modifiers.meta.wanted) { - // e.stopPropagation works in Firefox. - if (e.stopPropagation) { - e.stopPropagation(); - e.preventDefault(); - } - return false; - } - } - }; - // Store shortcut - keyboardManagerService.keyboardEvent[label] = { - 'callback': fct, - 'target': elt, - 'event': opt['type'] - }; + //found the right callback! - //Attach the function with the event - if(elt.addEventListener){ - elt.addEventListener(opt['type'], fct, false); - }else if(elt.attachEvent){ - elt.attachEvent('on' + opt['type'], fct); - }else{ - elt['on' + opt['type']] = fct; - } - }; - // Remove the shortcut - just specify the shortcut and I will remove the binding - keyboardManagerService.unbind = function (label) { - label = label.toLowerCase(); - var binding = keyboardManagerService.keyboardEvent[label]; - delete(keyboardManagerService.keyboardEvent[label]); - - if(!binding){return;} + // Disable event handler when focus input and textarea + if (opt['inputDisabled']) { + var elt; + if (e.target) { + elt = e.target; + } else if (e.srcElement) { + elt = e.srcElement; + } - var type = binding['event'], + if (elt.nodeType === 3) { elt = elt.parentNode; } + if (elt.tagName === 'INPUT' || elt.tagName === 'TEXTAREA') { + //This exits the Find loop + return true; + } + } + + $timeout(function () { + callback(e); + }, 1); + + if (!opt['propagate']) { // Stop the event + propagate = false; + } + + //This exits the Find loop + return true; + } + + //we haven't found one so continue looking + return false; + + }); + + // Stop the event if required + if (!propagate) { + // e.cancelBubble is supported by IE - this will kill the bubbling process. + e.cancelBubble = true; + e.returnValue = false; + + // e.stopPropagation works in Firefox. + if (e.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + return false; + } + } + + // Store all keyboard combination shortcuts + keyboardManagerService.keyboardEvent = {}; + + // Add a new keyboard combination shortcut + keyboardManagerService.bind = function (label, callback, opt) { + + //replace ctrl key with meta key + if(isMac && label !== "ctrl+space"){ + label = label.replace("ctrl","meta"); + } + + var elt; + // Initialize opt object + opt = angular.extend({}, defaultOpt, opt); + label = label.toLowerCase(); + elt = opt.target; + if(typeof opt.target === 'string'){ + elt = document.getElementById(opt.target); + } + + //Ensure we aren't double binding to the same element + type otherwise we'll end up multi-binding + // and raising events for now reason. So here we'll check if the event is already registered for the element + var boundValues = _.values(keyboardManagerService.keyboardEvent); + var found = _.find(boundValues, function (i) { + return i.target === elt && i.event === opt['type']; + }); + + // Store shortcut + keyboardManagerService.keyboardEvent[label] = { + 'callback': callback, + 'target': elt, + 'opt': opt + }; + + if (!found) { + //Attach the function with the event + if (elt.addEventListener) { + elt.addEventListener(opt['type'], eventHandler, false); + } else if (elt.attachEvent) { + elt.attachEvent('on' + opt['type'], eventHandler); + } else { + elt['on' + opt['type']] = eventHandler; + } + } + + }; + // Remove the shortcut - just specify the shortcut and I will remove the binding + keyboardManagerService.unbind = function (label) { + label = label.toLowerCase(); + var binding = keyboardManagerService.keyboardEvent[label]; + delete(keyboardManagerService.keyboardEvent[label]); + + if(!binding){return;} + + var type = binding['event'], elt = binding['target'], callback = binding['callback']; - - if(elt.detachEvent){ - elt.detachEvent('on' + type, callback); - }else if(elt.removeEventListener){ - elt.removeEventListener(type, callback, false); - }else{ - elt['on'+type] = false; - } - }; - // - return keyboardManagerService; -}]); \ No newline at end of file + if(elt.detachEvent){ + elt.detachEvent('on' + type, callback); + }else if(elt.removeEventListener){ + elt.removeEventListener(type, callback, false); + }else{ + elt['on'+type] = false; + } + }; + // + + return keyboardManagerService; +} angular.module('umbraco.services').factory('keyboardService', ['$window', '$timeout', keyboardService]); \ 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 new file mode 100644 index 0000000000..0bdc3bb524 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -0,0 +1,521 @@ +/** + @ngdoc service + * @name umbraco.services.listViewHelper + * + * + * @description + * Service for performing operations against items in the list view UI. Used by the built-in internal listviews + * as well as custom listview. + * + * A custom listview is always used inside a wrapper listview, so there are a number of inherited values on its + * scope by default: + * + * **$scope.selection**: Array containing all items currently selected in the listview + * + * **$scope.items**: Array containing all items currently displayed in the listview + * + * **$scope.folders**: Array containing all folders in the current listview (only for media) + * + * **$scope.options**: configuration object containing information such as pagesize, permissions, order direction etc. + * + * **$scope.model.config.layouts**: array of available layouts to apply to the listview (grid, list or custom layout) + * + * ##Usage## + * To use, inject listViewHelper into custom listview controller, listviewhelper expects you + * to pass in the full collection of items in the listview in several of its methods + * this collection is inherited from the parent controller and is available on $scope.selection + * + *
      + *      angular.module("umbraco").controller("my.listVieweditor". function($scope, listViewHelper){
      + *
      + *          //current items in the listview
      + *          var items = $scope.items;
      + *
      + *          //current selection
      + *          var selection = $scope.selection;
      + *
      + *          //deselect an item , $scope.selection is inherited, item is picked from inherited $scope.items
      + *          listViewHelper.deselectItem(item, $scope.selection);
      + *
      + *          //test if all items are selected, $scope.items + $scope.selection are inherited
      + *          listViewhelper.isSelectedAll($scope.items, $scope.selection);
      + *      });
      + * 
      + */ +(function () { + 'use strict'; + + function listViewHelper(localStorageService) { + + var firstSelectedIndex = 0; + var localStorageKey = "umblistViewLayout"; + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for internal use, based on the collection of layouts passed, the method selects either + * any previous layout from local storage, or picks the first allowed layout + * + * @param {Number} nodeId The id of the current node displayed in the content editor + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + + function getLayout(nodeId, availableLayouts) { + + var storedLayouts = []; + + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); + } + + if (storedLayouts && storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + return setLayout(nodeId, layout, availableLayouts); + } + } + + } + + return getFirstAllowedLayout(availableLayouts); + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Changes the current layout used by the listview to the layout passed in. Stores selection in localstorage + * + * @param {Number} nodeID Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + * @param {Array} availableLayouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + + function setLayout(nodeId, selectedLayout, availableLayouts) { + + var activeLayout = {}; + var layoutFound = false; + + for (var i = 0; availableLayouts.length > i; i++) { + var layout = availableLayouts[i]; + if (layout.path === selectedLayout.path) { + activeLayout = layout; + layout.active = true; + layoutFound = true; + } else { + layout.active = false; + } + } + + if (!layoutFound) { + activeLayout = getFirstAllowedLayout(availableLayouts); + } + + saveLayoutInLocalStorage(nodeId, activeLayout); + + return activeLayout; + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#saveLayoutInLocalStorage + * @methodOf umbraco.services.listViewHelper + * + * @description + * Stores a given layout as the current default selection in local storage + * + * @param {Number} nodeId Id of the current node displayed in the content editor + * @param {Object} selectedLayout Layout selected as the layout to set as the current layout + */ + + function saveLayoutInLocalStorage(nodeId, selectedLayout) { + var layoutFound = false; + var storedLayouts = []; + + if (localStorageService.get(localStorageKey)) { + storedLayouts = localStorageService.get(localStorageKey); + } + + if (storedLayouts.length > 0) { + for (var i = 0; storedLayouts.length > i; i++) { + var layout = storedLayouts[i]; + if (layout.nodeId === nodeId) { + layout.path = selectedLayout.path; + layoutFound = true; + } + } + } + + if (!layoutFound) { + var storageObject = { + "nodeId": nodeId, + "path": selectedLayout.path + }; + storedLayouts.push(storageObject); + } + + localStorageService.set(localStorageKey, storedLayouts); + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#getFirstAllowedLayout + * @methodOf umbraco.services.listViewHelper + * + * @description + * Returns currently selected layout, or alternatively the first layout in the available layouts collection + * + * @param {Array} layouts Array of all allowed layouts, available from $scope.model.config.layouts + */ + + function getFirstAllowedLayout(layouts) { + + var firstAllowedLayout = {}; + + for (var i = 0; layouts.length > i; i++) { + var layout = layouts[i]; + if (layout.selected === true) { + firstAllowedLayout = layout; + break; + } + } + + return firstAllowedLayout; + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectHandler + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for working with item selection via a checkbox, internally it uses selectItem and deselectItem. + * Working with this method, requires its triggered via a checkbox which can then pass in its triggered $event + * When the checkbox is clicked, this method will toggle selection of the associated item so it matches the state of the checkbox + * + * @param {Object} selectedItem Item being selected or deselected by the checkbox + * @param {Number} selectedIndex Index of item being selected/deselected, usually passed as $index + * @param {Array} items All items in the current listview, available as $scope.items + * @param {Array} selection All selected items in the current listview, available as $scope.selection + * @param {Event} $event Event triggered by the checkbox being checked to select / deselect an item + */ + + function selectHandler(selectedItem, selectedIndex, items, selection, $event) { + + var start = 0; + var end = 0; + var item = null; + + if ($event.shiftKey === true) { + + if (selectedIndex > firstSelectedIndex) { + + start = firstSelectedIndex; + end = selectedIndex; + + for (; end >= start; start++) { + item = items[start]; + selectItem(item, selection); + } + + } else { + + start = firstSelectedIndex; + end = selectedIndex; + + for (; end <= start; start--) { + item = items[start]; + selectItem(item, selection); + } + + } + + } else { + + if (selectedItem.selected) { + deselectItem(selectedItem, selection); + } else { + selectItem(selectedItem, selection); + } + + firstSelectedIndex = selectedIndex; + + } + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Selects a given item to the listview selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to select + * @param {Array} selection Listview selection, available as $scope.selection + */ + + function selectItem(item, selection) { + var isSelected = false; + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + if (item.id === selectedItem.id) { + isSelected = true; + } + } + if (!isSelected) { + selection.push({ id: item.id }); + item.selected = true; + } + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#deselectItem + * @methodOf umbraco.services.listViewHelper + * + * @description + * Deselects a given item from the listviews selection array, requires you pass in the inherited $scope.selection collection + * + * @param {Object} item Item to deselect + * @param {Array} selection Listview selection, available as $scope.selection + */ + + function deselectItem(item, selection) { + for (var i = 0; selection.length > i; i++) { + var selectedItem = selection[i]; + if (item.id === selectedItem.id) { + selection.splice(i, 1); + item.selected = false; + } + } + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#clearSelection + * @methodOf umbraco.services.listViewHelper + * + * @description + * Removes a given number of items and folders from the listviews selection array + * Folders can only be passed in if the listview is used in the media section which has a concept of folders. + * + * @param {Array} items Items to remove, can be null + * @param {Array} folders Folders to remove, can be null + * @param {Array} selection Listview selection, available as $scope.selection + */ + + function clearSelection(items, folders, selection) { + + var i = 0; + + selection.length = 0; + + if (angular.isArray(items)) { + for (i = 0; items.length > i; i++) { + var item = items[i]; + item.selected = false; + } + } + + if(angular.isArray(folders)) { + for (i = 0; folders.length > i; i++) { + var folder = folders[i]; + folder.selected = false; + } + } + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#selectAllItems + * @methodOf umbraco.services.listViewHelper + * + * @description + * Helper method for toggling the select state on all items in the active listview + * Can only be used from a checkbox as a checkbox $event is required to pass in. + * + * @param {Array} items Items to toggle selection on, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @param {$event} $event Event passed from the checkbox being toggled + */ + + function selectAllItems(items, selection, $event) { + + var checkbox = $event.target; + var clearSelection = false; + + if (!angular.isArray(items)) { + return; + } + + selection.length = 0; + + for (var i = 0; i < items.length; i++) { + + var item = items[i]; + + if (checkbox.checked) { + selection.push({ id: item.id }); + } else { + clearSelection = true; + } + + item.selected = checkbox.checked; + + } + + if (clearSelection) { + selection.length = 0; + } + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#isSelectedAll + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method to determine if all items on the current page in the list has been selected + * Given the current items in the view, and the current selection, it will return true/false + * + * @param {Array} items Items to test if all are selected, should be $scope.items + * @param {Array} selection Listview selection, available as $scope.selection + * @returns {Boolean} boolean indicate if all items in the listview have been selected + */ + + function isSelectedAll(items, selection) { + + var numberOfSelectedItem = 0; + + for (var itemIndex = 0; items.length > itemIndex; itemIndex++) { + var item = items[itemIndex]; + + for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { + var selectedItem = selection[selectedIndex]; + + if (item.id === selectedItem.id) { + numberOfSelectedItem++; + } + } + + } + + if (numberOfSelectedItem === items.length) { + return true; + } + + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setSortingDirection + * @methodOf umbraco.services.listViewHelper + * + * @description + * *Internal* method for changing sort order icon + * @param {String} col Column alias to order after + * @param {String} direction Order direction `asc` or `desc` + * @param {Object} options object passed from the parent listview available as $scope.options + */ + + function setSortingDirection(col, direction, options) { + return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction; + } + + /** + * @ngdoc method + * @name umbraco.services.listViewHelper#setSorting + * @methodOf umbraco.services.listViewHelper + * + * @description + * Method for setting the field on which the listview will order its items after. + * + * @param {String} field Field alias to order after + * @param {Boolean} allow Determines if the user is allowed to set this field, normally true + * @param {Object} options Options object passed from the parent listview available as $scope.options + */ + + function setSorting(field, allow, options) { + if (allow) { + if (options.orderBy === field && options.orderDirection === 'asc') { + options.orderDirection = "desc"; + } else { + options.orderDirection = "asc"; + } + options.orderBy = field; + } + } + + //This takes in a dictionary of Ids with Permissions and determines + // the intersect of all permissions to return an object representing the + // listview button permissions + function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) { + + if (currentIdsWithPermissions == null) { + currentIdsWithPermissions = {}; + } + + //merge the newly retrieved permissions to the main dictionary + _.each(unmergedPermissions, function (value, key, list) { + currentIdsWithPermissions[key] = value; + }); + + //get the intersect permissions + var arr = []; + _.each(currentIdsWithPermissions, function (value, key, list) { + arr.push(value); + }); + + //we need to use 'apply' to call intersection with an array of arrays, + //see: http://stackoverflow.com/a/16229480/694494 + var intersectPermissions = _.intersection.apply(_, arr); + + return { + canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O + canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C + canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D + canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M + canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U + canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; + } + + var service = { + + getLayout: getLayout, + getFirstAllowedLayout: getFirstAllowedLayout, + setLayout: setLayout, + saveLayoutInLocalStorage: saveLayoutInLocalStorage, + selectHandler: selectHandler, + selectItem: selectItem, + deselectItem: deselectItem, + clearSelection: clearSelection, + selectAllItems: selectAllItems, + isSelectedAll: isSelectedAll, + setSortingDirection: setSortingDirection, + setSorting: setSorting, + getButtonPermissions: getButtonPermissions + + }; + + return service; + + } + + + angular.module('umbraco.services').factory('listViewHelper', listViewHelper); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js index 027a055bba..7cfc79a083 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js @@ -1,6 +1,7 @@ angular.module('umbraco.services') .factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) { + //TODO: This should be injected as server vars var url = "LocalizedText"; var resourceFileLoadStatus = "none"; var resourceLoadingPromise = []; @@ -37,6 +38,11 @@ angular.module('umbraco.services') initLocalizedResources: function () { var deferred = $q.defer(); + if (resourceFileLoadStatus === "loaded") { + deferred.resolve(service.dictionary); + return deferred.promise; + } + //if the resource is already loading, we don't want to force it to load another one in tandem, we'd rather // wait for that initial http promise to finish and then return this one with the dictionary loaded if (resourceFileLoadStatus === "loading") { @@ -91,25 +97,13 @@ angular.module('umbraco.services') // checks the dictionary for a localized resource string localize: function (value, tokens) { - var deferred = $q.defer(); - - if (resourceFileLoadStatus === "loaded") { - var val = _lookup(value, tokens, service.dictionary); - deferred.resolve(val); - } else { - service.initLocalizedResources().then(function (dic) { - var val = _lookup(value, tokens, dic); - deferred.resolve(val); - }); - } - - return deferred.promise; + return service.initLocalizedResources().then(function (dic) { + var val = _lookup(value, tokens, dic); + return val; + }); }, - - }; - // force the load of the resource file - service.initLocalizedResources(); + }; //This happens after login / auth and assets loading eventsService.on("app.authenticated", function () { @@ -119,4 +113,4 @@ angular.module('umbraco.services') // return the local instance when called return service; -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js index f0f1649bbe..6e78d71e85 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js @@ -153,6 +153,51 @@ function macroService() { macroString += ")"; return macroString; + }, + + collectValueData: function(macro, macroParams, renderingEngine) { + + var paramDictionary = {}; + var macroAlias = macro.alias; + var syntax; + + _.each(macroParams, function (item) { + + var val = item.value; + + if (item.value !== null && item.value !== undefined && !_.isString(item.value)) { + try { + val = angular.toJson(val); + } + catch (e) { + // not json + } + } + + //each value needs to be xml escaped!! since the value get's stored as an xml attribute + paramDictionary[item.alias] = _.escape(val); + + }); + + //get the syntax based on the rendering engine + if (renderingEngine && renderingEngine === "WebForms") { + syntax = this.generateWebFormsSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); + } + else if (renderingEngine && renderingEngine === "Mvc") { + syntax = this.generateMvcSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); + } + else { + syntax = this.generateMacroSyntax({ macroAlias: macroAlias, macroParamsDictionary: paramDictionary }); + } + + var macroObject = { + "macroParamsDictionary": paramDictionary, + "macroAlias": macroAlias, + "syntax": syntax + }; + + return macroObject; + } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 68ca32efbc..114e1a0962 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -335,8 +335,37 @@ function mediaHelper(umbRequestHelper) { var lowered = imagePath.toLowerCase(); var ext = lowered.substr(lowered.lastIndexOf(".") + 1); return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; + }, + + /** + * @ngdoc function + * @name umbraco.services.mediaHelper#formatFileTypes + * @methodOf umbraco.services.mediaHelper + * @function + * + * @description + * Returns a string with correctly formated file types for ng-file-upload + * + * @param {string} file types, ex: jpg,png,tiff + */ + formatFileTypes: function(fileTypes) { + + var fileTypesArray = fileTypes.split(','); + var newFileTypesArray = []; + + for (var i = 0; i < fileTypesArray.length; i++) { + var fileType = fileTypesArray[i]; + + if (fileType.indexOf(".") !== 0) { + fileType = ".".concat(fileType); + } + + newFileTypesArray.push(fileType); + } + + return newFileTypesArray.join(","); + } }; -} -angular.module('umbraco.services').factory('mediaHelper', mediaHelper); \ No newline at end of file +}angular.module('umbraco.services').factory('mediaHelper', mediaHelper); 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 f404419340..9687e5c40d 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 @@ -2,14 +2,14 @@ * @ngdoc service * @name umbraco.services.navigationService * - * @requires $rootScope + * @requires $rootScope * @requires $routeParams * @requires $log * @requires $location * @requires dialogService * @requires treeService * @requires sectionResource - * + * * @description * Service to handle the main application navigation. Responsible for invoking the tree * Section navigation and search, and maintain their state for the entire application lifetime @@ -17,10 +17,10 @@ */ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeout, $injector, dialogService, umbModelMapper, treeService, notificationsService, historyService, appState, angularHelper) { - + //used to track the current dialog object var currentDialog = null; - + //the main tree event handler, which gets assigned via the setupTreeEvents method var mainTreeEventHandler = null; //tracks the user profile dialog @@ -35,8 +35,8 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo appState.setMenuState("showMenuDialog", false); appState.setGlobalState("stickyNavigation", false); appState.setGlobalState("showTray", false); - - //$("#search-form input").focus(); + + //$("#search-form input").focus(); break; case 'menu': appState.setGlobalState("navMode", "menu"); @@ -87,7 +87,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo /** initializes the navigation service */ init: function() { - //keep track of the current section - initially this will always be undefined so + //keep track of the current section - initially this will always be undefined so // no point in setting it now until it changes. $rootScope.$watch(function () { return $routeParams.section; @@ -95,7 +95,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo appState.setSectionState("currentSection", newVal); }); - + }, /** @@ -107,7 +107,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * Shows the legacy iframe and loads in the content based on the source url * @param {String} source The URL to load into the iframe */ - loadLegacyIFrame: function (source) { + loadLegacyIFrame: function (source) { $location.path("/" + appState.getSectionState("currentSection") + "/framed/" + encodeURIComponent(source)); }, @@ -149,7 +149,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo showTree: function (sectionAlias, syncArgs) { if (sectionAlias !== appState.getSectionState("currentSection")) { appState.setSectionState("currentSection", sectionAlias); - + if (syncArgs) { this.syncTree(syncArgs); } @@ -165,7 +165,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo appState.setGlobalState("showTray", false); }, - /** + /** Called to assign the main tree event handler - this is called by the navigation controller. TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way. */ @@ -179,7 +179,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //when a tree node is synced this event will fire, this allows us to set the currentNode mainTreeEventHandler.bind("treeSynced", function (ev, args) { - + if (args.activate === undefined || args.activate === true) { //set the current selected node appState.setTreeState("selectedNode", args.node); @@ -196,7 +196,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //Set the current action node (this is not the same as the current selected node!) appState.setMenuState("currentNode", args.node); - + if (args.event && args.event.altKey) { args.skipDefault = true; } @@ -214,13 +214,13 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //this reacts to tree items themselves being clicked //the tree directive should not contain any handling, simply just bubble events - mainTreeEventHandler.bind("treeNodeSelect", function(ev, args) { + mainTreeEventHandler.bind("treeNodeSelect", function (ev, args) { var n = args.node; ev.stopPropagation(); ev.preventDefault(); if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { - //this is a legacy tree node! + //this is a legacy tree node! var jsPrefix = "javascript:"; var js; if (n.metaData["jsClickCallback"].startsWith(jsPrefix)) { @@ -243,7 +243,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo else if (n.routePath) { //add action to the history service historyService.add({ name: n.name, link: n.routePath, icon: n.icon }); - + //put this node into the tree state appState.setTreeState("selectedNode", args.node); //when a node is clicked we also need to set the active menu node to this node @@ -269,7 +269,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * The path format is: ["itemId","itemId"], and so on * so to sync to a specific document type node do: *
      -         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});  
      +         * navigationService.syncTree({tree: 'content', path: ["-1","123d"], forceReload: true});
                * 
      * @param {Object} args arguments passed to the function * @param {String} args.tree the tree alias to sync to @@ -287,7 +287,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo if (!args.tree) { throw "args.tree cannot be null"; } - + if (mainTreeEventHandler) { //returns a promise return mainTreeEventHandler.syncTree(args); @@ -297,8 +297,8 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo return angularHelper.rejectedPromise(); }, - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to have to set an active tree and then sync, the new API does this in one method by using syncTree */ _syncPath: function(path, forceReload) { @@ -322,8 +322,8 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo } }, - /** - Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to + /** + Internal method that should ONLY be used by the legacy API wrapper, the legacy API used to have to set an active tree and then sync, the new API does this in one method by using syncTreePath */ _setActiveTreeType: function (treeAlias, loadChildren) { @@ -331,7 +331,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo mainTreeEventHandler._setActiveTreeType(treeAlias, loadChildren); } }, - + /** * @ngdoc method * @name umbraco.services.navigationService#hideTree @@ -356,7 +356,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * @methodOf umbraco.services.navigationService * * @description - * Hides the tree by hiding the containing dom element. + * Hides the tree by hiding the containing dom element. * This always returns a promise! * * @param {Event} event the click event triggering the method, passed from the DOM element @@ -382,7 +382,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //NOTE: This is assigning the current action node - this is not the same as the currently selected node! appState.setMenuState("currentNode", args.node); - + //ensure the current dialog is cleared before creating another! if (currentDialog) { dialogService.close(currentDialog); @@ -400,13 +400,13 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo } } - //there is no default or we couldn't find one so just continue showing the menu + //there is no default or we couldn't find one so just continue showing the menu setMode("menu"); appState.setMenuState("currentNode", args.node); appState.setMenuState("menuActions", data.menuItems); - appState.setMenuState("dialogTitle", args.node.name); + appState.setMenuState("dialogTitle", args.node.name); //we're not opening a dialog, return null. deferred.resolve(null); @@ -437,7 +437,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo throw "action cannot be null"; } if (!node) { - throw "node cannot be null"; + throw "node cannot be null"; } if (!section) { throw "section cannot be null"; @@ -456,9 +456,9 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo var menuAction = action.metaData["jsAction"].split('.'); if (menuAction.length !== 2) { - //if it is not two parts long then this most likely means that it's a legacy action + //if it is not two parts long then this most likely means that it's a legacy action var js = action.metaData["jsAction"].replace("javascript:", ""); - //there's not really a different way to acheive this except for eval + //there's not really a different way to acheive this except for eval eval(js); } else { @@ -551,7 +551,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo modalClass: "umb-modal-left", show: true }); - + return service.helpDialog; }, @@ -564,13 +564,13 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * Opens a dialog, for a given action on a given tree node * uses the dialogService to inject the selected action dialog * into #dialog div.umb-panel-body - * the path to the dialog view is determined by: + * the path to the dialog view is determined by: * "views/" + current tree + "/" + action alias + ".html" * The dialog controller will get passed a scope object that is created here with the properties: * scope.currentNode = the selected tree node * scope.currentAction = the selected menu item * so that the dialog controllers can use these properties - * + * * @param {Object} args arguments passed to the function * @param {Scope} args.scope current scope passed to the dialog * @param {Object} args.action the clicked action containing `name` and `alias` @@ -590,6 +590,7 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //ensure the current dialog is cleared before creating another! if (currentDialog) { dialogService.close(currentDialog); + currentDialog = null; } setMode("dialog"); @@ -649,14 +650,14 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo } //TODO: some action's want to launch a new window like live editing, we support this in the menu item's metadata with - // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog - // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, + // a key called: "actionUrlMethod" which can be set to either: Dialog, BlankWindow. Normally this is always set to Dialog + // if a URL is specified in the "actionUrl" metadata. For now I'm not going to implement launching in a blank window, // though would be v-easy, just not sure we want to ever support that? var dialog = dialogService.open( { container: $("#dialog div.umb-modalcolumn-body"), - //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is + //The ONLY reason we're passing in scope to the dialogService (which is legacy functionality) is // for backwards compatibility since many dialogs require $scope.currentNode or $scope.currentAction // to exist scope: dialogScope, @@ -685,9 +686,9 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo * hides the currently open dialog */ hideDialog: function (showMenu) { - + setMode("default"); - + if(showMenu){ this.showMenu(undefined, { skipDefault: true, node: appState.getMenuState("currentNode") }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlayhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlayhelper.service.js new file mode 100644 index 0000000000..6dd3de6cb8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlayhelper.service.js @@ -0,0 +1,37 @@ +(function() { + 'use strict'; + + function overlayHelper() { + + var numberOfOverlays = 0; + + function registerOverlay() { + numberOfOverlays++; + return numberOfOverlays; + } + + function unregisterOverlay() { + numberOfOverlays--; + return numberOfOverlays; + } + + function getNumberOfOverlays() { + return numberOfOverlays; + } + + var service = { + numberOfOverlays: numberOfOverlays, + registerOverlay: registerOverlay, + unregisterOverlay: unregisterOverlay, + getNumberOfOverlays: getNumberOfOverlays + }; + + return service; + + } + + + angular.module('umbraco.services').factory('overlayHelper', overlayHelper); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index a70544cb7e..cdc7d77ca4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -56,20 +56,22 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro * @param {Object} editor the TinyMCE editor instance * @param {Object} $scope the current controller scope */ - createInsertEmbeddedMedia: function (editor, $scope) { + createInsertEmbeddedMedia: function (editor, scope, callback) { editor.addButton('umbembeddialog', { icon: 'custom icon-tv', tooltip: 'Embed', onclick: function () { - dialogService.embedDialog({ - callback: function (data) { - editor.insertContent(data); - } - }); + if (callback) { + callback(); + } } }); }, + insertEmbeddedMediaInEditor: function(editor, preview) { + editor.insertContent(preview); + }, + /** * @ngdoc method * @name umbraco.services.tinyMceService#createMediaPicker @@ -81,7 +83,7 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro * @param {Object} editor the TinyMCE editor instance * @param {Object} $scope the current controller scope */ - createMediaPicker: function (editor) { + createMediaPicker: function (editor, scope, callback) { editor.addButton('umbmediapicker', { icon: 'custom icon-picture', tooltip: 'Media Picker', @@ -101,52 +103,48 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro } userService.getCurrentUser().then(function(userData) { - dialogService.mediaPicker({ - currentTarget: currentTarget, - onlyImages: true, - showDetails: true, - startNodeId: userData.startMediaId, - callback: function (img) { - - if (img) { - - var data = { - alt: img.altText || "", - src: (img.url) ? img.url : "nothing.jpg", - rel: img.id, - 'data-id': img.id, - id: '__mcenew' - }; - - editor.insertContent(editor.dom.createHTML('img', data)); - - $timeout(function () { - var imgElm = editor.dom.get('__mcenew'); - var size = editor.dom.getSize(imgElm); - - if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { - var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); - - var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; - editor.dom.setAttrib(imgElm, 'style', s); - editor.dom.setAttrib(imgElm, 'id', null); - - if (img.url) { - var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; - editor.dom.setAttrib(imgElm, 'data-mce-src', src); - } - } - }, 500); - } - } - }); + if(callback) { + callback(currentTarget, userData); + } }); - } }); }, + insertMediaInEditor: function(editor, img) { + if(img) { + + var data = { + alt: img.altText || "", + src: (img.url) ? img.url : "nothing.jpg", + rel: img.id, + 'data-id': img.id, + id: '__mcenew' + }; + + editor.insertContent(editor.dom.createHTML('img', data)); + + $timeout(function () { + var imgElm = editor.dom.get('__mcenew'); + var size = editor.dom.getSize(imgElm); + + if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) { + var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h); + + var s = "width: " + newSize.width + "px; height:" + newSize.height + "px;"; + editor.dom.setAttrib(imgElm, 'style', s); + editor.dom.setAttrib(imgElm, 'id', null); + + if (img.url) { + var src = img.url + "?width=" + newSize.width + "&height=" + newSize.height; + editor.dom.setAttrib(imgElm, 'data-mce-src', src); + } + } + }, 500); + } + }, + /** * @ngdoc method * @name umbraco.services.tinyMceService#createUmbracoMacro @@ -158,8 +156,10 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro * @param {Object} editor the TinyMCE editor instance * @param {Object} $scope the current controller scope */ - createInsertMacro: function (editor, $scope) { - + createInsertMacro: function (editor, $scope, callback) { + + var createInsertMacroScope = this; + /** Adds custom rules for the macro plugin and custom serialization */ editor.on('preInit', function (args) { //this is requires so that we tell the serializer that a 'div' is actually allowed in the root, otherwise the cleanup will strip it out @@ -195,45 +195,6 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro return null; } - /** loads in the macro content async from the server */ - function loadMacroContent($macroDiv, macroData) { - - //if we don't have the macroData, then we'll need to parse it from the macro div - if (!macroData) { - var contents = $macroDiv.contents(); - var comment = _.find(contents, function (item) { - return item.nodeType === 8; - }); - if (!comment) { - throw "Cannot parse the current macro, the syntax in the editor is invalid"; - } - var syntax = comment.textContent.trim(); - var parsed = macroService.parseMacroSyntax(syntax); - macroData = parsed; - } - - var $ins = $macroDiv.find("ins"); - - //show the throbber - $macroDiv.addClass("loading"); - - var contentId = $routeParams.id; - - //need to wrap in safe apply since this might be occuring outside of angular - angularHelper.safeApply($scope, function() { - macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) - .then(function (htmlResult) { - - $macroDiv.removeClass("loading"); - htmlResult = htmlResult.trim(); - if (htmlResult !== "") { - $ins.html(htmlResult); - } - }); - }); - - } - /** Adds the button instance */ editor.addButton('umbmacro', { icon: 'custom icon-settings-alt', @@ -336,8 +297,8 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro //get all macro divs and load their content $(editor.dom.select(".umb-macro-holder.mceNonEditable")).each(function() { - loadMacroContent($(this)); - }); + createInsertMacroScope.loadMacroContent($(this), null, $scope); + }); }); @@ -460,33 +421,355 @@ function tinyMceService(dialogService, $log, imageHelper, $http, $timeout, macro }; } - dialogService.macroPicker({ - dialogData : dialogData, - callback: function(data) { - - //put the macro syntax in comments, we will parse this out on the server side to be used - //for persisting. - var macroSyntaxComment = ""; - //create an id class for this element so we can re-select it after inserting - var uniqueId = "umb-macro-" + editor.dom.uniqueId(); - var macroDiv = editor.dom.create('div', - { - 'class': 'umb-macro-holder ' + data.macroAlias + ' mceNonEditable ' + uniqueId - }, - macroSyntaxComment + 'Macro alias: ' + data.macroAlias + ''); - - editor.selection.setNode(macroDiv); - - var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); - - //async load the macro content - loadMacroContent($macroDiv, data); - } - }); + if(callback) { + callback(dialogData); + } } }); + }, + + insertMacroInEditor: function(editor, macroObject, $scope) { + + //put the macro syntax in comments, we will parse this out on the server side to be used + //for persisting. + var macroSyntaxComment = ""; + //create an id class for this element so we can re-select it after inserting + var uniqueId = "umb-macro-" + editor.dom.uniqueId(); + var macroDiv = editor.dom.create('div', + { + 'class': 'umb-macro-holder ' + macroObject.macroAlias + ' mceNonEditable ' + uniqueId + }, + macroSyntaxComment + 'Macro alias: ' + macroObject.macroAlias + ''); + + editor.selection.setNode(macroDiv); + + var $macroDiv = $(editor.dom.select("div.umb-macro-holder." + uniqueId)); + + //async load the macro content + this.loadMacroContent($macroDiv, macroObject, $scope); + + }, + + /** loads in the macro content async from the server */ + loadMacroContent: function($macroDiv, macroData, $scope) { + + //if we don't have the macroData, then we'll need to parse it from the macro div + if (!macroData) { + var contents = $macroDiv.contents(); + var comment = _.find(contents, function (item) { + return item.nodeType === 8; + }); + if (!comment) { + throw "Cannot parse the current macro, the syntax in the editor is invalid"; + } + var syntax = comment.textContent.trim(); + var parsed = macroService.parseMacroSyntax(syntax); + macroData = parsed; + } + + var $ins = $macroDiv.find("ins"); + + //show the throbber + $macroDiv.addClass("loading"); + + var contentId = $routeParams.id; + + //need to wrap in safe apply since this might be occuring outside of angular + angularHelper.safeApply($scope, function() { + macroResource.getMacroResultAsHtmlForEditor(macroData.macroAlias, contentId, macroData.macroParamsDictionary) + .then(function (htmlResult) { + + $macroDiv.removeClass("loading"); + htmlResult = htmlResult.trim(); + if (htmlResult !== "") { + $ins.html(htmlResult); + } + }); + }); + + }, + + createLinkPicker: function(editor, $scope, onClick) { + + function createLinkList(callback) { + return function() { + var linkList = editor.settings.link_list; + + if (typeof(linkList) === "string") { + tinymce.util.XHR.send({ + url: linkList, + success: function(text) { + callback(tinymce.util.JSON.parse(text)); + } + }); + } else { + callback(linkList); + } + }; + } + + function showDialog(linkList) { + var data = {}, selection = editor.selection, dom = editor.dom, selectedElm, anchorElm, initialText; + var win, linkListCtrl, relListCtrl, targetListCtrl; + + function linkListChangeHandler(e) { + var textCtrl = win.find('#text'); + + if (!textCtrl.value() || (e.lastControl && textCtrl.value() === e.lastControl.text())) { + textCtrl.value(e.control.text()); + } + + win.find('#href').value(e.control.value()); + } + + function buildLinkList() { + var linkListItems = [{ + text: 'None', + value: '' + }]; + + tinymce.each(linkList, function(link) { + linkListItems.push({ + text: link.text || link.title, + value: link.value || link.url, + menu: link.menu + }); + }); + + return linkListItems; + } + + function buildRelList(relValue) { + var relListItems = [{ + text: 'None', + value: '' + }]; + + tinymce.each(editor.settings.rel_list, function(rel) { + relListItems.push({ + text: rel.text || rel.title, + value: rel.value, + selected: relValue === rel.value + }); + }); + + return relListItems; + } + + function buildTargetList(targetValue) { + var targetListItems = [{ + text: 'None', + value: '' + }]; + + if (!editor.settings.target_list) { + targetListItems.push({ + text: 'New window', + value: '_blank' + }); + } + + tinymce.each(editor.settings.target_list, function(target) { + targetListItems.push({ + text: target.text || target.title, + value: target.value, + selected: targetValue === target.value + }); + }); + + return targetListItems; + } + + function buildAnchorListControl(url) { + var anchorList = []; + + tinymce.each(editor.dom.select('a:not([href])'), function(anchor) { + var id = anchor.name || anchor.id; + + if (id) { + anchorList.push({ + text: id, + value: '#' + id, + selected: url.indexOf('#' + id) !== -1 + }); + } + }); + + if (anchorList.length) { + anchorList.unshift({ + text: 'None', + value: '' + }); + + return { + name: 'anchor', + type: 'listbox', + label: 'Anchors', + values: anchorList, + onselect: linkListChangeHandler + }; + } + } + + function updateText() { + if (!initialText && data.text.length === 0) { + this.parent().parent().find('#text')[0].value(this.value()); + } + } + + selectedElm = selection.getNode(); + anchorElm = dom.getParent(selectedElm, 'a[href]'); + + data.text = initialText = anchorElm ? (anchorElm.innerText || anchorElm.textContent) : selection.getContent({format: 'text'}); + data.href = anchorElm ? dom.getAttrib(anchorElm, 'href') : ''; + data.target = anchorElm ? dom.getAttrib(anchorElm, 'target') : ''; + data.rel = anchorElm ? dom.getAttrib(anchorElm, 'rel') : ''; + + if (selectedElm.nodeName === "IMG") { + data.text = initialText = " "; + } + + if (linkList) { + linkListCtrl = { + type: 'listbox', + label: 'Link list', + values: buildLinkList(), + onselect: linkListChangeHandler + }; + } + + if (editor.settings.target_list !== false) { + targetListCtrl = { + name: 'target', + type: 'listbox', + label: 'Target', + values: buildTargetList(data.target) + }; + } + + if (editor.settings.rel_list) { + relListCtrl = { + name: 'rel', + type: 'listbox', + label: 'Rel', + values: buildRelList(data.rel) + }; + } + + var injector = angular.element(document.getElementById("umbracoMainPageBody")).injector(); + var dialogService = injector.get("dialogService"); + var currentTarget = null; + + //if we already have a link selected, we want to pass that data over to the dialog + if(anchorElm){ + var anchor = $(anchorElm); + currentTarget = { + name: anchor.attr("title"), + url: anchor.attr("href"), + target: anchor.attr("target") + }; + + //locallink detection, we do this here, to avoid poluting the dialogservice + //so the dialog service can just expect to get a node-like structure + if(currentTarget.url.indexOf("localLink:") > 0){ + currentTarget.id = currentTarget.url.substring(currentTarget.url.indexOf(":")+1,currentTarget.url.length-1); + } + } + + if(onClick) { + onClick(currentTarget, anchorElm); + } + + } + + editor.addButton('link', { + icon: 'link', + tooltip: 'Insert/edit link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]' + }); + + editor.addButton('unlink', { + icon: 'unlink', + tooltip: 'Remove link', + cmd: 'unlink', + stateSelector: 'a[href]' + }); + + editor.addShortcut('Ctrl+K', '', createLinkList(showDialog)); + this.showDialog = showDialog; + + editor.addMenuItem('link', { + icon: 'link', + text: 'Insert link', + shortcut: 'Ctrl+K', + onclick: createLinkList(showDialog), + stateSelector: 'a[href]', + context: 'insert', + prependToContext: true + }); + + }, + + insertLinkInEditor: function(editor, target, anchorElm) { + + var href = target.url; + + function insertLink() { + if (anchorElm) { + editor.dom.setAttribs(anchorElm, { + href: href, + title: target.name, + target: target.target ? target.target : null, + rel: target.rel ? target.rel : null, + 'data-id': target.id ? target.id : null + }); + + editor.selection.select(anchorElm); + editor.execCommand('mceEndTyping'); + } else { + editor.execCommand('mceInsertLink', false, { + href: href, + title: target.name, + target: target.target ? target.target : null, + rel: target.rel ? target.rel : null, + 'data-id': target.id ? target.id : null + }); + } + } + + if (!href) { + editor.execCommand('unlink'); + return; + } + + //if we have an id, it must be a locallink:id, aslong as the isMedia flag is not set + if(target.id && (angular.isUndefined(target.isMedia) || !target.isMedia)){ + href = "/{localLink:" + target.id + "}"; + insertLink(); + return; + } + + // Is email and not //user@domain.com + if (href.indexOf('@') > 0 && href.indexOf('//') === -1 && href.indexOf('mailto:') === -1) { + href = 'mailto:' + href; + insertLink(); + return; + } + + // Is www. prefixed + if (/^\s*www\./i.test(href)) { + href = 'http://' + href; + insertLink(); + return; + } + + insertLink(); + } + }; } 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 f0548faac7..ccdd283ea6 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 @@ -3,7 +3,7 @@ * @name umbraco.services.umbRequestHelper * @description A helper object used for sending requests to the server **/ -function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService) { +function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService, eventsService) { return { /** @@ -158,9 +158,10 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ //show a ysod dialog if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { - dialogService.ysodDialog({ - errorMsg: result.errorMsg, - data: result.data + eventsService.emit('app.ysod', + { + errorMsg: 'An error occured', + data: data }); } else { @@ -253,8 +254,9 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ } else if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { //show a ysod dialog - dialogService.ysodDialog({ - errorMsg: 'An error occurred', + eventsService.emit('app.ysod', + { + errorMsg: 'An error occured', data: data }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 7ad3f0e289..dffda24f1d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') - .factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper) { + .factory('userService', function ($rootScope, eventsService, $q, $location, $log, securityRetryQueue, authResource, dialogService, $timeout, angularHelper, $http) { var currentUser = null; var lastUserId = null; @@ -211,7 +211,7 @@ angular.module('umbraco.services') return result; }); }, - + /** Logs the user out */ logout: function () { @@ -250,7 +250,7 @@ angular.module('umbraco.services') } setCurrentUser(data); - currentUser.avatar = 'https://www.gravatar.com/avatar/' + data.emailHash + '?s=40&d=404'; + deferred.resolve(currentUser); }); 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 008d0c2183..9fbf2947af 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 @@ -21,6 +21,7 @@ function packageHelper(assetsService, treeService, eventsService, $templateCache } 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 */ @@ -319,7 +320,6 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me } }; } - angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper); /** @@ -487,10 +487,57 @@ angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorH function umbDataFormatter() { return { + formatContentTypePostData: function (displayModel, action) { + + //create the save model from the display model + var saveModel = _.pick(displayModel, + 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', + 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', + 'key', 'parentId', 'alias', 'path'); + + //TODO: Map these + saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); + saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; + var realGroups = _.reject(displayModel.groups, function(g) { + //do not include these tabs + return g.tabState === "init"; + }); + saveModel.groups = _.map(realGroups, function (g) { + + var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name'); + + var realProperties = _.reject(g.properties, function (p) { + //do not include these properties + return p.propertyState === "init" || p.inherited === true; + }); + + var saveProperties = _.map(realProperties, function (p) { + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile'); + return saveProperty; + }); + + saveGroup.properties = saveProperties; + + //if this is an inherited group and there are not non-inherited properties on it, then don't send up the data + if (saveGroup.inherited === true && saveProperties.length === 0) { + return null; + } + + return saveGroup; + }); + + //we don't want any null groups + saveModel.groups = _.reject(saveModel.groups, function(g) { + return !g; + }); + + return saveModel; + }, + /** formats the display model used to display the data type to the model used to save the data type */ formatDataTypePostData: function(displayModel, preValues, action) { var saveModel = { - parentId: -1, + parentId: displayModel.parentId, id: displayModel.id, name: displayModel.name, selectedEditor: displayModel.selectedEditor, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/xmlhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/xmlhelper.service.js index 03f1f6cbbe..a8562c833e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/xmlhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/xmlhelper.service.js @@ -372,9 +372,6 @@ function xmlhelper($http) { fromJson: function (json) { var xml = x2js.json2xml_str(json); return xml; - }, - parseFeed: function (url) { - return $http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&num=50&callback=JSON_CALLBACK&q=' + encodeURIComponent(url)); } }; } 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 08185a156a..0d3b56991e 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -13,9 +13,13 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ //the null is important because we do an explicit bool check on this in the view //the avatar is by default the umbraco logo $scope.authenticated = null; - $scope.avatar = "assets/img/application/logo.png"; + $scope.avatar = [ + { value: "assets/img/application/logo.png" }, + { value: "assets/img/application/logo@2x.png" }, + { value: "assets/img/application/logo@3x.png" } + ]; $scope.touchDevice = appState.getGlobalState("touchDevice"); - + $scope.removeNotification = function (index) { notificationsService.remove(index); @@ -24,12 +28,12 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ $scope.closeDialogs = function (event) { //only close dialogs if non-link and non-buttons are clicked var el = event.target.nodeName; - var els = ["INPUT","A","BUTTON"]; + var els = ["INPUT", "A", "BUTTON"]; - if(els.indexOf(el) >= 0){return;} + if (els.indexOf(el) >= 0) { return; } var parents = $(event.target).parents("a,button"); - if(parents.length > 0){ + if (parents.length > 0) { return; } @@ -45,31 +49,31 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ var evts = []; //when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function() { + evts.push(eventsService.on("app.notAuthenticated", function () { $scope.authenticated = null; $scope.user = null; })); - + //when the app is read/user is logged in, setup the data evts.push(eventsService.on("app.ready", function (evt, data) { - + $scope.authenticated = data.authenticated; $scope.user = data.user; - updateChecker.check().then(function(update){ - if(update && update !== "null"){ - if(update.type !== "None"){ + updateChecker.check().then(function(update) { + if (update && update !== "null") { + if (update.type !== "None") { var notification = { - headline: "Update available", - message: "Click to download", - sticky: true, - type: "info", - url: update.url + headline: "Update available", + message: "Click to download", + sticky: true, + type: "info", + url: update.url }; notificationsService.add(notification); } } - }) + }); //if the user has changed we need to redirect to the root so they don't try to continue editing the //last item in the URL (NOTE: the user id can equal zero, so we cannot just do !data.lastUserId since that will resolve to true) @@ -84,22 +88,44 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ tmhDynamicLocale.set($scope.user.locale); } - if($scope.user.emailHash){ - $timeout(function () { - //yes this is wrong.. - $("#avatar-img").fadeTo(1000, 0, function () { - $timeout(function () { - //this can be null if they time out - if ($scope.user && $scope.user.emailHash) { - $scope.avatar = "https://www.gravatar.com/avatar/" + $scope.user.emailHash + ".jpg?s=64&d=mm"; - } - }); - $("#avatar-img").fadeTo(1000, 1); - }); - - }, 3000); - } + if ($scope.user.emailHash) { + //let's attempt to load the avatar, it might not exist or we might not have + // internet access, well get an empty string back + $http.get(umbRequestHelper.getApiUrl("gravatarApiBaseUrl", "GetCurrentUserGravatarUrl")) + .then( + function successCallback(response) { + // if we can't download the gravatar for some reason, an null gets returned, we cannot do anything + if (response.data !== "null") { + $("#avatar-img").fadeTo(1000, 0, function () { + $scope.$apply(function () { + //this can be null if they time out + if ($scope.user && $scope.user.emailHash) { + var avatarBaseUrl = "https://www.gravatar.com/avatar/", + hash = $scope.user.emailHash; + + $scope.avatar = [ + { value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" }, + { value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" }, + { value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" } + ]; + } + }); + $("#avatar-img").fadeTo(1000, 1); + }); + } + }, function errorCallback(response) { + //cannot load it from the server so we cannot do anything + }); + } + })); + + evts.push(eventsService.on("app.ysod", function (name, error) { + $scope.ysodOverlay = { + view: "ysod", + error: error, + show: true + }; })); //ensure to unregister from all events! diff --git a/src/Umbraco.Web.UI.Client/src/index.html b/src/Umbraco.Web.UI.Client/src/index.html index 362510e044..50d78c315b 100644 --- a/src/Umbraco.Web.UI.Client/src/index.html +++ b/src/Umbraco.Web.UI.Client/src/index.html @@ -9,18 +9,17 @@ - +
      -
      -
      +
      - + diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index cb79c8e402..ded5ba08e4 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -9,8 +9,8 @@ app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', beforeSend: function (xhr) { xhr.setRequestHeader("X-XSRF-TOKEN", $cookies["XSRF-TOKEN"]); } - }); - + }); + /** Listens for authentication and checks if our required assets are loaded, if/once they are we'll broadcast a ready event */ eventsService.on("app.authenticated", function(evt, data) { assetsService._loadInitAssets().then(function() { @@ -24,14 +24,14 @@ app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', /** execute code on each successful route */ $rootScope.$on('$routeChangeSuccess', function(event, current, previous) { - + if(current.params.section){ $rootScope.locationTitle = current.params.section + " - " + $location.$$host; } else { $rootScope.locationTitle = "Umbraco - " + $location.$$host; } - + //reset the editorState on each successful route chage editorState.reset(); @@ -49,25 +49,17 @@ app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', var returnPath = null; if (rejection.path == "/login" || rejection.path.startsWith("/login/")) { //Set the current path before redirecting so we know where to redirect back to - returnPath = encodeURIComponent($location.url()); + returnPath = encodeURIComponent($location.url()); } $location.path(rejection.path) if (returnPath) { $location.search("returnPath", returnPath); } - + }); - /** For debug mode, always clear template cache to cut down on - dev frustration and chrome cache on templates */ - if(Umbraco.Sys.ServerVariables.isDebuggingEnabled){ - $rootScope.$on('$viewContentLoaded', function() { - $templateCache.removeAll(); - }); - } - /* this will initialize the navigation service once the application has started */ navigationService.init(); @@ -75,4 +67,4 @@ app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', //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); - }]); \ No newline at end of file + }]); 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 970cf8da31..4b732bf56e 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -17,9 +17,9 @@ 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 250.000 websites are currently powered by Umbraco', + 'Over 300 000 websites are currently powered by Umbraco', "At least 2 people have named their cat 'Umbraco'", - 'On an average day, more than 1.000 people download 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', 'You can find the world\'s friendliest CMS community at our.umbraco.org', 'You can become a certified Umbraco developer by attending one of the official courses', @@ -31,9 +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 400 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", + "More than 400 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 and using either JavaScript or C#" + "You can extend Umbraco without modifying the source code using either JavaScript or C#", + "Umbraco was installed in more than 165 countries in 2015" ]; /** diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index 0e9ebc7526..0cc7511f89 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -42,7 +42,7 @@
      - + Enter server domain or IP
      diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less index 1f725946d2..2beb855560 100644 --- a/src/Umbraco.Web.UI.Client/src/less/alerts.less +++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less @@ -31,6 +31,10 @@ line-height: @baseLineHeight; } +.close.-align-right { + right: 0; +} + // Alternate styles // ------------------------- @@ -63,11 +67,15 @@ } .alert-form { - background-color: @grayLighter; + background-color: #ECECEC; border: 1px solid @gray !important; color: @gray; } +.alert-form.-no-border { + border: none !important; +} + .alert-form h4 { color: @gray; } diff --git a/src/Umbraco.Web.UI.Client/src/less/animations.less b/src/Umbraco.Web.UI.Client/src/less/animations.less deleted file mode 100644 index ee17d37194..0000000000 --- a/src/Umbraco.Web.UI.Client/src/less/animations.less +++ /dev/null @@ -1,191 +0,0 @@ -// Animations -// ------------------------- - -.fade-hide, .fade-show , .fade-leave, .fade-enter { - -webkit-transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; - -moz-transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; - -o-transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; - transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; -} -.fade-enter { - opacity: 1; -} - -.fade-hide.fade-hide-active { - opacity: 0; -} -.fade-leave { - opacity: 0; -} - -.fade-show.fade-show-active { - opacity: 1; -} - -.slide-hide, .slide-show { - -webkit-transition: all cubic-bezier(0.770, 0.000, 0.175, 1.000) 0.5s; - -moz-transition: all cubic-bezier(0.770, 0.000, 0.175, 1.000) 0.5s; - -o-transition: all cubic-bezier(0.770, 0.000, 0.175, 1.000) 0.5s; - transition: all cubic-bezier(0.770, 0.000, 0.175, 1.000) 0.5s; -} - -.slide-hide { - -webkit-transform: translateX(0%); - -moz-transform: translateX(0%); - -ms-transform: translateX(0%); - transform: translateX(0%); -} - -.slide-hide.slide-hide-active { - -webkit-transform: translateX(-100%); - -moz-transform: translateX(-100%); - -ms-transform: translateX(-100%); - transform: translateX(-100%); -} - -.slide-show { - -webkit-transform: translateX(-100%); - -moz-transform: translateX(-100%); - -ms-transform: translateX(-100%); - transform: translateX(-100%); -} - -.slide-show.slide-show-active { - -webkit-transform: translateX(0%); - -moz-transform: translateX(0%); - -ms-transform: translateX(0%); - transform: translateX(0%); -} - -.tree-node-delete-leave { - -webkit-animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); - -moz-animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); - -o-animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); - animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); - display: block; - position: relative; -} - -@-webkit-keyframes leave { - to { - opacity: 0; - height: 0px; - bottom: -70px; - } - 25% { - bottom: 15px; - } - from { - opacity: 1; - height: 30px; - bottom: 0px; - } -} -@-moz-keyframes leave { - to { - opacity: 0; - height: 0px; - bottom: -70px; - } - 25% { - bottom: 15px; - } - from { - opacity: 1; - height: 30px; - bottom: 0px; - } -} -@-ms-keyframes leave { - to { - opacity: 0; - height: 0px; - bottom: -70px; - } - 25% { - bottom: 15px; - } - from { - opacity: 1; - height: 30px; - bottom: 0px; - } -} -@-o-keyframes leave { - to { - opacity: 0; - height: 0px; - bottom: -70px; - } - 25% { - bottom: 15px; - } - from { - opacity: 1; - height: 30px; - bottom: 0px; - } -} -@keyframes leave { - to { - opacity: 0; - height: 0px; - bottom: -70px; - } - 25% { - bottom: 15px; - } - from { - opacity: 1; - height: 30px; - bottom: 0px; - } -} - -.tree-node-delete-leave * { - color:@red !important; -} - - -.tree-node-slide-up -{ - opacity:1; - top: 0px; - -webkit-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; - -moz-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; - -ms-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; - -o-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; - transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; -} -.tree-node-slide-up * { - font-size:100%; - -webkit-transition:font-size 700ms; - -moz-transition:font-size 700ms; - -ms-transition:font-size 700ms; - -o-transition:font-size 700ms; - transition:font-size 700ms; -} -.tree-node-slide-up.tree-node-slide-up-hide-active { - opacity: 0; - top: -100px; -} -.tree-node-slide-up.tree-node-slide-up-hide-active * { - font-size:120%; -} - -.tree-fade-out-hide , -.tree-fade-out-show, -.tree-fade-out-hide div:not(.tree-node-slide-up-hide-active), -.tree-fade-out-show div:not(.tree-node-slide-up-hide-active) { - -webkit-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; - -moz-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; - -ms-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; - -o-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; - transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; -} -.tree-fade-out-show.tree-fade-out-show-active div:not(.tree-node-slide-up-hide-active){ - opacity: 1; -} -.tree-fade-out-hide.tree-fade-out-hide-active div:not(.tree-node-slide-up-hide-active){ - opacity: 0; -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/application/animations.less b/src/Umbraco.Web.UI.Client/src/less/application/animations.less new file mode 100644 index 0000000000..dae428c18a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/application/animations.less @@ -0,0 +1,180 @@ +// Animations +// ------------------------- + +//Animate.css - http://daneden.me/animate +//Licensed under the MIT license - http://opensource.org/licenses/MIT +//Copyright (c) 2013 Daniel Eden +.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}.animated.infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animated.hinge{-webkit-animation-duration:2s;animation-duration:2s}@-webkit-keyframes bounce{0%,100%,20%,53%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1);-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}40%,43%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}70%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}@keyframes bounce{0%,100%,20%,53%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1);-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}40%,43%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-30px,0);-ms-transform:translate3d(0,-30px,0);transform:translate3d(0,-30px,0)}70%{-webkit-transition-timing-function:cubic-bezier(0.755,.050,.855,.060);transition-timing-function:cubic-bezier(0.755,.050,.855,.060);-webkit-transform:translate3d(0,-15px,0);-ms-transform:translate3d(0,-15px,0);transform:translate3d(0,-15px,0)}90%{-webkit-transform:translate3d(0,-4px,0);-ms-transform:translate3d(0,-4px,0);transform:translate3d(0,-4px,0)}}.bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;-ms-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,100%,50%{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,100%,50%{opacity:1}25%,75%{opacity:0}}.flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes pulse{0%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);-ms-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}100%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.pulse{-webkit-animation-name:pulse;animation-name:pulse}@-webkit-keyframes rubberBand{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(0.75,1.25,1);transform:scale3d(0.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes rubberBand{0%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}30%{-webkit-transform:scale3d(1.25,.75,1);-ms-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(0.75,1.25,1);-ms-transform:scale3d(0.75,1.25,1);transform:scale3d(0.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);-ms-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);-ms-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);-ms-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}100%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shake{0%,100%{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);-ms-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);-ms-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.shake{-webkit-animation-name:shake;animation-name:shake}@-webkit-keyframes swing{20%{-webkit-transform:rotate3d(0,0,1,15deg);transform:rotate3d(0,0,1,15deg)}40%{-webkit-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}60%{-webkit-transform:rotate3d(0,0,1,5deg);transform:rotate3d(0,0,1,5deg)}80%{-webkit-transform:rotate3d(0,0,1,-5deg);transform:rotate3d(0,0,1,-5deg)}100%{-webkit-transform:rotate3d(0,0,1,0deg);transform:rotate3d(0,0,1,0deg)}}@keyframes swing{20%{-webkit-transform:rotate3d(0,0,1,15deg);-ms-transform:rotate3d(0,0,1,15deg);transform:rotate3d(0,0,1,15deg)}40%{-webkit-transform:rotate3d(0,0,1,-10deg);-ms-transform:rotate3d(0,0,1,-10deg);transform:rotate3d(0,0,1,-10deg)}60%{-webkit-transform:rotate3d(0,0,1,5deg);-ms-transform:rotate3d(0,0,1,5deg);transform:rotate3d(0,0,1,5deg)}80%{-webkit-transform:rotate3d(0,0,1,-5deg);-ms-transform:rotate3d(0,0,1,-5deg);transform:rotate3d(0,0,1,-5deg)}100%{-webkit-transform:rotate3d(0,0,1,0deg);-ms-transform:rotate3d(0,0,1,0deg);transform:rotate3d(0,0,1,0deg)}}.swing{-webkit-transform-origin:top center;-ms-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg);transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)}100%{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes tada{0%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg);-ms-transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg);transform:scale3d(.9,.9,.9) rotate3d(0,0,1,-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg);-ms-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg);-ms-transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg);transform:scale3d(1.1,1.1,1.1) rotate3d(0,0,1,-3deg)}100%{-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{-webkit-transform:none;transform:none}}@keyframes wobble{0%{-webkit-transform:none;-ms-transform:none;transform:none}15%{-webkit-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);-ms-transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg);transform:translate3d(-25%,0,0) rotate3d(0,0,1,-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);-ms-transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg);transform:translate3d(20%,0,0) rotate3d(0,0,1,3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);-ms-transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg);transform:translate3d(-15%,0,0) rotate3d(0,0,1,-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);-ms-transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg);transform:translate3d(10%,0,0) rotate3d(0,0,1,2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);-ms-transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg);transform:translate3d(-5%,0,0) rotate3d(0,0,1,-1deg)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes bounceIn{0%,100%,20%,40%,60%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes bounceIn{0%,100%,20%,40%,60%,80%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);-ms-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);-ms-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);-ms-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);-ms-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);-ms-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}.bounceIn{-webkit-animation-name:bounceIn;animation-name:bounceIn;-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounceInDown{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}100%{-webkit-transform:none;transform:none}}@keyframes bounceInDown{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0);-ms-transform:translate3d(0,-3000px,0);transform:translate3d(0,-3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0);-ms-transform:translate3d(0,25px,0);transform:translate3d(0,25px,0)}75%{-webkit-transform:translate3d(0,-10px,0);-ms-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}90%{-webkit-transform:translate3d(0,5px,0);-ms-transform:translate3d(0,5px,0);transform:translate3d(0,5px,0)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}100%{-webkit-transform:none;transform:none}}@keyframes bounceInLeft{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0);-ms-transform:translate3d(-3000px,0,0);transform:translate3d(-3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0);-ms-transform:translate3d(25px,0,0);transform:translate3d(25px,0,0)}75%{-webkit-transform:translate3d(-10px,0,0);-ms-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}90%{-webkit-transform:translate3d(5px,0,0);-ms-transform:translate3d(5px,0,0);transform:translate3d(5px,0,0)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}100%{-webkit-transform:none;transform:none}}@keyframes bounceInRight{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0);-ms-transform:translate3d(3000px,0,0);transform:translate3d(3000px,0,0)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0);-ms-transform:translate3d(-25px,0,0);transform:translate3d(-25px,0,0)}75%{-webkit-transform:translate3d(10px,0,0);-ms-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}90%{-webkit-transform:translate3d(-5px,0,0);-ms-transform:translate3d(-5px,0,0);transform:translate3d(-5px,0,0)}100%{-webkit-transform:none;-ms-transform:none;transform:none}}.bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}@keyframes bounceInUp{0%,100%,60%,75%,90%{-webkit-transition-timing-function:cubic-bezier(0.215,.61,.355,1);transition-timing-function:cubic-bezier(0.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0);-ms-transform:translate3d(0,3000px,0);transform:translate3d(0,3000px,0)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0);-ms-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}75%{-webkit-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}90%{-webkit-transform:translate3d(0,-5px,0);-ms-transform:translate3d(0,-5px,0);transform:translate3d(0,-5px,0)}100%{-webkit-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);-ms-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);-ms-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.bounceOut{-webkit-animation-name:bounceOut;animation-name:bounceOut;-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0);-ms-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0);-ms-transform:translate3d(0,-20px,0);transform:translate3d(0,-20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);-ms-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0);-ms-transform:translate3d(20px,0,0);transform:translate3d(20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);-ms-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0);-ms-transform:translate3d(-20px,0,0);transform:translate3d(-20px,0,0)}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);-ms-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0);-ms-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0);-ms-transform:translate3d(0,20px,0);transform:translate3d(0,20px,0)}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);-ms-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes fadeIn{0%{opacity:0}100%{opacity:1}}.fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);-ms-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);-ms-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);-ms-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);-ms-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);-ms-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);-ms-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);-ms-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes fadeOut{0%{opacity:1}100%{opacity:0}}.fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,100%,0);-ms-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,2000px,0);-ms-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-100%,0,0);-ms-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);-ms-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0);-ms-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(2000px,0,0);-ms-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-100%,0);-ms-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);-ms-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-360deg);transform:perspective(400px) rotate3d(0,1,0,-360deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}100%{-webkit-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-360deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-360deg);transform:perspective(400px) rotate3d(0,1,0,-360deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);-ms-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);-ms-transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);transform:perspective(400px) translate3d(0,0,150px) rotate3d(0,1,0,-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95);-ms-transform:perspective(400px) scale3d(.95,.95,.95);transform:perspective(400px) scale3d(.95,.95,.95);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}100%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animated.flip{-webkit-backface-visibility:visible;-ms-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-ms-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-ms-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);-ms-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);-ms-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}}.flipInX{-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);-ms-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);-webkit-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);-ms-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}}.flipInY{-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-ms-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-ms-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}.flipOutX{-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);-ms-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);-ms-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);-ms-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}.flipOutY{-webkit-backface-visibility:visible!important;-ms-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY;-webkit-animation-duration:.75s;animation-duration:.75s}@-webkit-keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg);opacity:1}100%{-webkit-transform:none;transform:none;opacity:1}}@keyframes lightSpeedIn{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);-ms-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);-ms-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);-ms-transform:skewX(-5deg);transform:skewX(-5deg);opacity:1}100%{-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.lightSpeedIn{-webkit-animation-name:lightSpeedIn;animation-name:lightSpeedIn;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOut{0%{opacity:1}100%{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOut{0%{opacity:1}100%{-webkit-transform:translate3d(100%,0,0) skewX(30deg);-ms-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.lightSpeedOut{-webkit-animation-name:lightSpeedOut;animation-name:lightSpeedOut;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,-200deg);transform:rotate3d(0,0,1,-200deg);opacity:0}100%{-webkit-transform-origin:center;transform-origin:center;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateIn{0%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,-200deg);-ms-transform:rotate3d(0,0,1,-200deg);transform:rotate3d(0,0,1,-200deg);opacity:0}100%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);-ms-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,45deg);-ms-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,45deg);-ms-transform:rotate3d(0,0,1,45deg);transform:rotate3d(0,0,1,45deg);opacity:0}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-90deg);transform:rotate3d(0,0,1,-90deg);opacity:0}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;transform:none;opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-90deg);-ms-transform:rotate3d(0,0,1,-90deg);transform:rotate3d(0,0,1,-90deg);opacity:0}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:none;-ms-transform:none;transform:none;opacity:1}}.rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight}@-webkit-keyframes rotateOut{0%{-webkit-transform-origin:center;transform-origin:center;opacity:1}100%{-webkit-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,200deg);transform:rotate3d(0,0,1,200deg);opacity:0}}@keyframes rotateOut{0%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;opacity:1}100%{-webkit-transform-origin:center;-ms-transform-origin:center;transform-origin:center;-webkit-transform:rotate3d(0,0,1,200deg);-ms-transform:rotate3d(0,0,1,200deg);transform:rotate3d(0,0,1,200deg);opacity:0}}.rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut}@-webkit-keyframes rotateOutDownLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate(0,0,1,45deg);transform:rotate(0,0,1,45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate(0,0,1,45deg);-ms-transform:rotate(0,0,1,45deg);transform:rotate(0,0,1,45deg);opacity:0}}.rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft}@-webkit-keyframes rotateOutDownRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,-45deg);-ms-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}.rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight}@-webkit-keyframes rotateOutUpLeft{0%{-webkit-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;opacity:1}100%{-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:rotate3d(0,0,1,-45deg);-ms-transform:rotate3d(0,0,1,-45deg);transform:rotate3d(0,0,1,-45deg);opacity:0}}.rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft}@-webkit-keyframes rotateOutUpRight{0%{-webkit-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,90deg);transform:rotate3d(0,0,1,90deg);opacity:0}}@keyframes rotateOutUpRight{0%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;opacity:1}100%{-webkit-transform-origin:right bottom;-ms-transform-origin:right bottom;transform-origin:right bottom;-webkit-transform:rotate3d(0,0,1,90deg);-ms-transform:rotate3d(0,0,1,90deg);transform:rotate3d(0,0,1,90deg);opacity:0}}.rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight}@-webkit-keyframes hinge{0%{-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate3d(0,0,1,80deg);transform:rotate3d(0,0,1,80deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate3d(0,0,1,60deg);transform:rotate3d(0,0,1,60deg);-webkit-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}100%{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-transform-origin:top left;-ms-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate3d(0,0,1,80deg);-ms-transform:rotate3d(0,0,1,80deg);transform:rotate3d(0,0,1,80deg);-webkit-transform-origin:top left;-ms-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate3d(0,0,1,60deg);-ms-transform:rotate3d(0,0,1,60deg);transform:rotate3d(0,0,1,60deg);-webkit-transform-origin:top left;-ms-transform-origin:top left;transform-origin:top left;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}100%{-webkit-transform:translate3d(0,700px,0);-ms-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.hinge{-webkit-animation-name:hinge;animation-name:hinge}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg);transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg)}100%{opacity:1;-webkit-transform:none;transform:none}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg);-ms-transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg);transform:translate3d(-100%,0,0) rotate3d(0,0,1,-120deg)}100%{opacity:1;-webkit-transform:none;-ms-transform:none;transform:none}}.rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg);transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg)}}@keyframes rollOut{0%{opacity:1}100%{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg);-ms-transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg);transform:translate3d(100%,0,0) rotate3d(0,0,1,120deg)}}.rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-ms-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-ms-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}100%{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);-ms-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}100%{opacity:0}}.zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-transform-origin:center bottom;-ms-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;transform-origin:left center}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);-ms-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0);-webkit-transform-origin:left center;-ms-transform-origin:left center;transform-origin:left center}}.zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;transform-origin:right center}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);-ms-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}100%{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);-ms-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0);-webkit-transform-origin:right center;-ms-transform-origin:right center;transform-origin:right center}}.zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-ms-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(0.55,.055,.675,.19);animation-timing-function:cubic-bezier(0.55,.055,.675,.19)}100%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-ms-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-transform-origin:center bottom;-ms-transform-origin:center bottom;transform-origin:center bottom;-webkit-animation-timing-function:cubic-bezier(0.175,.885,.32,1);animation-timing-function:cubic-bezier(0.175,.885,.32,1)}}.zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp} + +.animated.-half-second { + animation-duration: 0.5s; +} + +.slide-in-left.ng-hide-remove { + -webkit-animation: fadeInLeft 0.6s; + animation: fadeInLeft 0.6s; +} + +.slide-in-left.ng-hide-add { + -webkit-animation: fadeOutLeft 0.6s; + animation: fadeOutLeft 0.6s; + display: block !important; +} + +.slide-in-right.ng-hide-remove { + -webkit-animation: fadeInRight 0.6s; + animation: fadeInRight 0.6s; +} + +.slide-in-right.ng-hide-add { + -webkit-animation: fadeOutRight 0.6s; + animation: fadeOutRight 0.6s; + display: block !important; +} + +.slide-in-up.ng-hide-remove { + -webkit-animation: fadeInUp 0.6s; + animation: fadeInUp 0.6s; +} + +.slide-in-up.ng-hide-add { + -webkit-animation: fadeOutDown 0.6s; + animation: fadeOutDown 0.6s; + display: block !important; +} + + +// TREE ANIMATION + +.tree-node-delete-leave { + -webkit-animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); + -moz-animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); + -o-animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); + animation: leave 600ms cubic-bezier(0.445, 0.050, 0.550, 0.950); + display: block; + position: relative; +} + +@-webkit-keyframes leave { + to { + opacity: 0; + height: 0px; + bottom: -70px; + } + 25% { + bottom: 15px; + } + from { + opacity: 1; + height: 30px; + bottom: 0px; + } +} +@-moz-keyframes leave { + to { + opacity: 0; + height: 0px; + bottom: -70px; + } + 25% { + bottom: 15px; + } + from { + opacity: 1; + height: 30px; + bottom: 0px; + } +} +@-ms-keyframes leave { + to { + opacity: 0; + height: 0px; + bottom: -70px; + } + 25% { + bottom: 15px; + } + from { + opacity: 1; + height: 30px; + bottom: 0px; + } +} +@-o-keyframes leave { + to { + opacity: 0; + height: 0px; + bottom: -70px; + } + 25% { + bottom: 15px; + } + from { + opacity: 1; + height: 30px; + bottom: 0px; + } +} +@keyframes leave { + to { + opacity: 0; + height: 0px; + bottom: -70px; + } + 25% { + bottom: 15px; + } + from { + opacity: 1; + height: 30px; + bottom: 0px; + } +} + +.tree-node-delete-leave * { + color:@red !important; +} + + +.tree-node-slide-up +{ + opacity:1; + top: 0px; + -webkit-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; + -moz-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; + -ms-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; + -o-transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; + transition: 700ms cubic-bezier(0.000, 0.000, 0.580, 1.000) all; +} +.tree-node-slide-up * { + font-size:100%; + -webkit-transition:font-size 700ms; + -moz-transition:font-size 700ms; + -ms-transition:font-size 700ms; + -o-transition:font-size 700ms; + transition:font-size 700ms; +} +.tree-node-slide-up.tree-node-slide-up-hide-active { + opacity: 0; + top: -100px; +} +.tree-node-slide-up.tree-node-slide-up-hide-active * { + font-size:120%; +} + +.tree-fade-out-hide , +.tree-fade-out-show, +.tree-fade-out-hide div:not(.tree-node-slide-up-hide-active), +.tree-fade-out-show div:not(.tree-node-slide-up-hide-active) { + -webkit-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; + -moz-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; + -ms-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; + -o-transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; + transition: 700ms cubic-bezier(0.075, 0.820, 0.165, 1.000) all; +} +.tree-fade-out-show.tree-fade-out-show-active div:not(.tree-node-slide-up-hide-active){ + opacity: 1; +} +.tree-fade-out-hide.tree-fade-out-hide-active div:not(.tree-node-slide-up-hide-active){ + opacity: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less similarity index 79% rename from src/Umbraco.Web.UI.Client/src/less/grid.less rename to src/Umbraco.Web.UI.Client/src/less/application/grid.less index 5871412ee2..02f56e4046 100644 --- a/src/Umbraco.Web.UI.Client/src/less/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -1,269 +1,287 @@ -// Grid -// ------------------------- - -/* CONTAINS BASIC APPLICATION LAYOUT, POSITIONING AND AREA DIMENSIONS */ - -html, body { - height: 100%; - overflow: hidden; -} - -body { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - - font-family: @baseFontFamily; - font-size: @baseFontSize; - line-height: @baseLineHeight; - color: @textColor; - background-color: @bodyBackground; -} - - -.padded { - padding: 20px -} - - -#layout { - position: relative; - height: 100%; - padding: 0; - z-index: 1; -} - -#mainwrapper { - height: 100%; - width: 100%; - margin: 0; -} - -#contentwrapper, #contentcolumn { - position: absolute; - top: 0px; bottom: 0px; right: 0px; left: 80px; - z-index: 10; - margin: 0 -} - -#contentcolumn { - left: 0px; -} - -#contentcolumn iframe#right { - display: block; - position: relative; - height: 100%; - width: 100%; - border: none; -} - -#leftcolumn { - height: 100%; - z-index: 20; - width: 80px; - float: left; - position: absolute; -} - -#applications { - z-index: 1000; - height: 100%; - left: 0px; - top: 0; - bottom: 0; - position: absolute; - text-align: center -} - -#applications-tray { - z-index: 900; - left: 80px; - top: 0; - bottom: 0; - position: absolute; - height: 100%; - text-align: center; -} - -#search-form { - display: block; - margin: 0px; - z-index: 100; - position: absolute; - top: 0; - left: 0; - right: 0; -} - -#search-form form{ - margin-top: 10px; -} - -#navigation { - left: 80px; - top: 0; - bottom: 0; - position: absolute; - z-index: 100; - background: @white; - height: 100%; - -} - -.navigation-inner-container{ - position: absolute; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; - padding-top: 100px; - border-right: 1px solid @grayLight; - z-index: 100; -} - -#dialog { - min-width: 500px; - left: 100%; - top: 0; - position: absolute; - z-index: 50; - display: inline-block; -} - -#tree { - padding: 0px; - z-index: 100 !important; - overflow: auto; -} - -#tree .umb-tree { - padding: 0px 0px 20px 0px; -} - -#search-results { - z-index: 200; -} - -#contextMenu { - z-index: 50; - position: absolute; - top: 0px; - left: 100%; - min-width: 250px; -} - -#speechbubble { - z-index: 1000; - position: absolute; - bottom: 100px; - left: 0; - right: 0; - border-bottom: none; - margin: auto; - padding: 0px; - border: none; - background: none; - border-radius: 0; -} - -.ui-resizable-e { - cursor: e-resize; - width: 4px; - right: -5px; - top: 0; - bottom: 0; - background-color: @grayLighter; - border: solid 1px @grayLight; - position:absolute; - z-index:9999 !important; - -} - -@media (min-width: 1101px) { - #contentwrapper {left: 440px;} - #speechbubble {left: 360px;} -} - -//empty section modification -.emptySection #contentwrapper {left: 80px;} -.emptySection #speechbubble {left: 0;} -.emptySection #navigation {display: none} - -.login-only #speechbubble { - z-index: 10000; - left: 0 !important; -} -.login-only #speechbubble ul { - padding-left:20px -} - - - -@media (max-width: 767px) { - - // make leftcolumn smaller on tablets - #leftcolumn { - width: 60px; - } - #applications ul.sections { - width: 60px; - } - ul.sections.sections-tray { - width: 60px; - } - #applications-tray { - left: 60px; - } - #navigation { - left: 60px; - } - #contentwrapper, #contentcolumn { - left: 30px; - } - #umbracoMainPageBody .umb-modal-left.fade.in { - margin-left: 60px; - } -} - - - -@media (max-width: 500px) { - - // make leftcolumn smaller on mobiles - #leftcolumn { - width: 40px; - } - #applications ul.sections { - width: 40px; - } - ul.sections li [class^="icon-"]:before { - font-size: 25px!important; - } - #applications ul.sections li.avatar a img { - width: 25px; - } - ul.sections a span { - display:none !important; - } - #applications ul.sections-tray { - width: 40px; - } - ul.sections.sections-tray { - width: 40px; - } - #applications-tray { - left: 40px; - } - #navigation { - left: 40px; - } - #contentwrapper, #contentcolumn { - left: 20px; - } - #umbracoMainPageBody .umb-modal-left.fade.in { - margin-left: 40px; - width: 85%!important; - } -} \ No newline at end of file +// Grid +// ------------------------- + +/* CONTAINS BASIC APPLICATION LAYOUT, POSITIONING AND AREA DIMENSIONS */ + +html, body { + height: 100%; + overflow: hidden; +} + +body { + margin: 0; + padding: 0; + height: 100%; + width: 100%; + + font-family: @baseFontFamily; + font-size: @baseFontSize; + line-height: @baseLineHeight; + color: @textColor; + background-color: @bodyBackground; +} + + +.padded { + padding: 20px +} + + +#layout { + position: relative; + height: 100%; + padding: 0; + z-index: 1; +} + +#mainwrapper { + height: 100%; + width: 100%; + margin: 0; +} + +#contentwrapper, #contentcolumn { + position: absolute; + top: 0px; bottom: 0px; right: 0px; left: 80px; + z-index: 10; + margin: 0 +} + +#umb-notifications-wrapper { + left: 80px; +} + +#contentcolumn { + left: 0px; +} + +#contentcolumn iframe#right { + display: block; + position: relative; + height: 100%; + width: 100%; + border: none; +} + +#leftcolumn { + height: 100%; + z-index: 20; + width: 80px; + float: left; + position: absolute; +} + +#applications { + z-index: 1000; + height: 100%; + left: 0px; + top: 0; + bottom: 0; + position: absolute; + text-align: center +} + +#applications-tray { + z-index: 900; + left: 80px; + top: 0; + bottom: 0; + position: absolute; + height: 100%; + text-align: center; +} + +#search-form { + display: block; + margin: 0; + z-index: 100; + position: absolute; + top: 0; + left: 0; + right: 0; + overflow: hidden; + + .form-search { + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: flex; + margin-top: 17px; + + .umb-search-field { + width: auto; + min-width: 160px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + } + } +} + +#navigation { + left: 80px; + top: 0; + bottom: 0; + position: absolute; + z-index: 100; + background: @white; + height: 100%; + +} + +.navigation-inner-container{ + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + padding-top: 100px; + border-right: 1px solid @grayLight; + z-index: 100; +} + +#dialog { + min-width: 500px; + left: 100%; + top: 0; + position: absolute; + z-index: 50; + display: inline-block; +} + +#tree { + padding: 0px; + z-index: 100 !important; + overflow: auto; +} + +#tree .umb-tree { + padding: 0px 0px 20px 0px; +} + +#search-results { + z-index: 200; +} + +#contextMenu { + z-index: 50; + position: absolute; + top: 0px; + left: 100%; + min-width: 250px; +} + +#speechbubble { + z-index: 1060; + position: absolute; + bottom: 100px; + left: 0; + right: 0; + border-bottom: none; + margin: auto; + padding: 0px; + border: none; + background: none; + border-radius: 0; +} + +.ui-resizable-e { + cursor: e-resize; + width: 4px; + right: -5px; + top: 0; + bottom: 0; + background-color: @grayLighter; + border: solid 1px @grayLight; + border-top: none; + border-bottom: none; + position:absolute; + z-index:9999 !important; +} + +@media (min-width: 1101px) { + #contentwrapper, #umb-notifications-wrapper {left: 440px;} + #speechbubble {left: 360px;} +} + +//empty section modification +.emptySection #contentwrapper, .emptySection #umb-notifications-wrapper {left: 80px;} +.emptySection #speechbubble {left: 0;} +.emptySection #navigation {display: none} + +.login-only #speechbubble { + z-index: 10000; + left: 0 !important; +} +.login-only #speechbubble ul { + padding-left:20px +} + + + +@media (max-width: 767px) { + + // make leftcolumn smaller on tablets + #leftcolumn { + width: 60px; + } + #applications ul.sections { + width: 60px; + } + ul.sections.sections-tray { + width: 60px; + } + #applications-tray { + left: 60px; + } + #navigation { + left: 60px; + } + #contentwrapper, #contentcolumn, #umb-notifications-wrapper { + left: 30px; + } + #umbracoMainPageBody .umb-modal-left.fade.in { + margin-left: 60px; + } +} + + + +@media (max-width: 500px) { + + // make leftcolumn smaller on mobiles + #leftcolumn { + width: 40px; + } + #applications ul.sections { + width: 40px; + } + ul.sections li [class^="icon-"]:before { + font-size: 25px!important; + } + #applications ul.sections li.avatar a img { + width: 25px; + } + ul.sections a span { + display:none !important; + } + #applications ul.sections-tray { + width: 40px; + } + ul.sections.sections-tray { + width: 40px; + } + #applications-tray { + left: 40px; + } + #navigation { + left: 40px; + } + #contentwrapper, #contentcolumn, #umb-notifications-wrapper { + left: 20px; + } + #umbracoMainPageBody .umb-modal-left.fade.in { + margin-left: 40px; + width: 85%!important; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/application/shadows.less b/src/Umbraco.Web.UI.Client/src/less/application/shadows.less new file mode 100644 index 0000000000..3df8ca5758 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/application/shadows.less @@ -0,0 +1,15 @@ +.shadow-depth-1{ + box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); +} +.shadow-depth-2{ + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); +} +.shadow-depth-3{ + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); +} +.shadow-depth-4{ + box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22); +} +.shadow-depth-5{ + box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22); +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b605968753..9d01cae4b9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -1,3 +1,4 @@ + // Core variables and mixins @import "fonts.less"; // Loading fonts @import "variables.less"; // Modify this for custom colors, font-sizes, etc @@ -34,7 +35,6 @@ @import "../../lib/bootstrap/less/pagination.less"; @import "../../lib/bootstrap/less/pager.less"; - // Components: Popovers @import "../../lib/bootstrap/less/modals.less"; @import "../../lib/bootstrap/less/tooltip.less"; @@ -50,12 +50,17 @@ @import "../../lib/bootstrap/less/carousel.less"; @import "../../lib/bootstrap/less/hero-unit.less"; + // Utility classes @import "../../lib/bootstrap/less/utilities.less"; // Has to be last to override when necessary + +// Application wide styles (refactor is WIP) +@import "application/grid.less"; +@import "application/shadows.less"; +@import "application/animations.less"; + // Belle styles -@import "grid.less"; -@import "login.less"; @import "buttons.less"; @import "forms.less"; @import "modals.less"; @@ -67,12 +72,57 @@ @import "listview.less"; @import "gridview.less"; @import "footer.less"; -@import "animations.less"; @import "dragdrop.less"; -@import "dashboards.less"; +@import "dashboards.less"; + +@import "forms/umb-validation-label.less"; + +// Umbraco Components +@import "components/editor.less"; +@import "components/overlays.less"; +@import "components/card.less"; +@import "components/umb-sub-views.less"; +@import "components/umb-editor-navigation.less"; +@import "components/umb-editor-sub-views.less"; +@import "components/editor/subheader/umb-editor-sub-header.less"; +@import "components/umb-grid-selector.less"; +@import "components/umb-child-selector.less"; +@import "components/umb-group-builder.less"; +@import "components/umb-list-view-settings.less"; +@import "components/umb-table.less"; +@import "components/umb-confirm-action.less"; +@import "components/umb-keyboard-shortcuts-overview.less"; +@import "components/umb-checkbox-list.less"; +@import "components/umb-locked-field.less"; +@import "components/umb-tabs.less"; +@import "components/umb-load-indicator.less"; +@import "components/umb-breadcrumbs.less"; +@import "components/umb-media-grid.less"; +@import "components/umb-folder-grid.less"; +@import "components/umb-content-grid.less"; +@import "components/umb-layout-selector.less"; +@import "components/tooltip/umb-tooltip.less"; +@import "components/tooltip/umb-tooltip-list.less"; +@import "components/overlays/umb-overlay-backdrop.less"; +@import "components/umb-grid.less"; +@import "components/umb-empty-state.less"; +@import "components/umb-property-editor.less"; +@import "components/umb-iconpicker.less"; + +@import "components/buttons/umb-button.less"; +@import "components/buttons/umb-button-group.less"; + +@import "components/notifications/umb-notifications.less"; +@import "components/umb-file-dropzone.less"; + +//page specific styles +@import "pages/document-type-editor.less"; +@import "pages/login.less"; + //used for property editors @import "property-editors.less"; + @import "typeahead.less"; @import "hacks.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index 3fac647746..84b96292c2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -9,7 +9,6 @@ // Core .btn { display: inline-block; - .ie7-inline-block(); padding: 4px 12px; margin-bottom: 0; // For input.btn font-size: @baseFontSize; @@ -20,47 +19,43 @@ background: @btnBackground; color: @black; border: 1px solid @btnBorder; - *border: 0; // Remove the border to prevent IE7's black border on input:focus - .border-radius(@baseBorderRadius); - .ie7-restore-left-whitespace(); // Give IE7 some love - -webkit-box-shadow: none; box-shadow: none; + // Hover/focus state + &:hover, + &:focus { + background: @btnBackgroundHighlight; + color: @grayDark; + background-position: 0 -15px; + text-decoration: none; - // Hover/focus state - &:hover, - &:focus { - background: @btnBackgroundHighlight; - color: @grayDark; - text-decoration: none; - background-position: 0 -15px; + // transition is only when going to hover/focus, otherwise the background + // behind the gradient (there for IE<=9 fallback) gets mismatched + .transition(background-position .1s linear); + } - // transition is only when going to hover/focus, otherwise the background - // behind the gradient (there for IE<=9 fallback) gets mismatched - .transition(background-position .1s linear); - } + // Focus state for keyboard and accessibility + &:focus { + .tab-focus(); + } - // Focus state for keyboard and accessibility - &:focus { - .tab-focus(); - } + // Active state + &.active, + &:active { + background-image: none; + outline: 0; + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + } - // Active state - &.active, - &:active { - background-image: none; - outline: 0; - .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); - } - - // Disabled state - &.disabled, - &[disabled] { - cursor: default; - background-image: none; - .opacity(65); - .box-shadow(none); - } + // Disabled state + &.disabled, + &[disabled], + &:disabled:hover { + cursor: default; + border-color: @btnBorder; + .opacity(65); + .box-shadow(none); + } } @@ -69,6 +64,12 @@ -webkit-box-shadow:none; } +.btn-group > .btn:first-child, +.btn-group > .btn:last-child, +.btn-group > .dropdown-toggle { + border-radius: 0; +} + // Button Sizes @@ -78,11 +79,11 @@ .btn-large { padding: @paddingLarge; font-size: @fontSizeLarge; - .border-radius(@borderRadiusLarge); } .btn-large [class^="icon-"], .btn-large [class*=" icon-"] { margin-top: 4px; + .border-radius(@borderRadiusLarge); } // Small @@ -91,6 +92,7 @@ font-size: @fontSizeSmall; .border-radius(@borderRadiusSmall); } + .btn-small [class^="icon-"], .btn-small [class*=" icon-"] { margin-top: 0; @@ -146,7 +148,6 @@ input[type="button"] { height: 32px; width: 32px; overflow: hidden; - text-decoration: none; display: inline-block; z-index: 6666; } @@ -292,3 +293,12 @@ input[type="submit"].btn { text-decoration:none; } } + +.btn-link.-underline { + display: inline-block; + text-decoration: underline; + + &:hover { + text-decoration: none; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/canvasdesigner.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less similarity index 98% rename from src/Umbraco.Web.UI.Client/src/less/canvasdesigner.less rename to src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index b28522db1c..52c4052975 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvasdesigner.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -2,8 +2,8 @@ /******* font-face *******/ @font-face { - src: url('/Umbraco/assets/fonts/helveticons/helveticons.eot') !important; - src: url('/Umbraco/assets/fonts/helveticons/helveticons.eot?#iefix') format('embedded-opentype'), url('/Umbraco/assets/fonts/helveticons/helveticons.ttf') format('truetype'), url('/Umbraco/assets/fonts/helveticons/helveticons.svg#icomoon') format('svg') !important; + src: url('assets/fonts/helveticons/helveticons.eot') !important; + src: url('assets/fonts/helveticons/helveticons.eot?#iefix') format('embedded-opentype'), url('assets/fonts/helveticons/helveticons.ttf') format('truetype'), url('assets/fonts/helveticons/helveticons.svg#icomoon') format('svg') !important; } /****************************/ 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 new file mode 100644 index 0000000000..0b063d094d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less @@ -0,0 +1,10 @@ +.umb-button-group__toggle { + padding-left: 8px; + padding-right: 8px; + margin-left: -1px; +} + +.umb-button-group__sub-buttons.-align-right { + right: 0; + left: auto; +} 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 new file mode 100644 index 0000000000..96bcfe077a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -0,0 +1,101 @@ +.umb-button { + position: relative; + overflow: hidden; +} + + .umb-button__button:focus { + outline: none; +} + +.umb-button__content { + opacity: 1; + transition: opacity 0.25s ease; + + display: flex; + flex-wrap: wrap; +} + +.umb-button__icon { + margin-right: 5px; +} + +.umb-button__content.-hidden { + opacity: 0; +} + +.umb-button__progress { + position: absolute; + left: 50%; + top: 50%; + width: 14px; + height: 14px; + margin-left: -9px; + margin-top: -9px; + z-index: 100; + border-radius: 40px; + border: 2px solid @btnBorder; + border-left-color: @green; + opacity: 1; + animation: rotating 0.4s linear infinite; + transition: opacity 0.25s ease; +} + +.umb-button__progress.-hidden { + opacity: 0; + z-index: 0; +} + +.umb-button__progress.-white { + border-color: rgba(255, 255, 255, 0.4); + border-left-color: #ffffff; +} + +.umb-button__success, +.umb-button__error { + position: absolute; + top: 50%; + left: 50%; + z-index: 10; + transform: translate(-50%, -50%); + opacity: 1; + font-size: 20px; + color: @green; + transition: opacity 0.25s ease; +} + +.umb-button__success { + color: @green; +} + +.umb-button__error { + color: @red; +} + +.umb-button__success.-hidden, +.umb-button__error.-hidden { + opacity: 0; + z-index: 0; +} + +.umb-button__success.-white, +.umb-button__error.-white { + color: #ffffff; +} + +.umb-button__overlay { + position: absolute; + width: 100%; + height: 100%; + z-index: 10; + background: #ffffff; + opacity: 0; +} + +@keyframes rotating { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less new file mode 100644 index 0000000000..85a076636f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -0,0 +1,185 @@ +/* + Library of card related compoents, like the right-hand icon list on the grid "cards" +*/ + +.umb-card{ + position: relative; + padding: 5px 10px 5px 10px; + background: white; + + .title{padding: 12px; color: @gray; border-bottom: 1px solid @grayLight; font-weight: 400; font-size: 16px; text-transform: none; margin: 0 -10px 10px -10px;} + +} + +.umb-card-thumb{ + text-align: center; + + i{ + text-align: center; + font-size: 20px; + line-height: 40px; + color: @blue; + display: block; + padding-top: 5px + } +} + +.umb-card-content{ + .item-title{color: @blackLight; font-weight: 400; border: none; font-size: 16px; text-transform: none; margin-bottom: 3px;} + p{color: @gray; margin-bottom: 1px;} +} + +.umb-card-actions{ + padding-top: 10px; + border-top: @grayLighter 1px solid; + clear: both; +} + +.umb-card-icons{ + text-align: center; + vertical-align: middle; + display: block; + list-style: none; + margin: 0; + padding: 0; +} + +.umb-card-icons.vertical{ + position: absolute; + top: 7px; + right: 7px; + text-align: right; + width: 1px; +} + +.umb-card-icons li{ + display: inline-block; + margin: 0 2px 0 2px; +} + +.umb-card-icons.vertical li{ + float: right; + display: block; + margin-bottom: 3px; +} + +//card iocn list +.umb-card-list{ + display: block; + padding: 0; + margin: 0; + } + +.umb-card-list li{ + border-bottom: @grayLighter 1px solid; + padding-bottom: 3px; + display: block; +} + + + + +//Card icon grid for picking items off a card +.umb-card-grid{ + padding: 0; + margin: 0 auto; + list-style: none; + + display: flex; + flex-flow: row wrap; + justify-content: flex-start; +} + +.umb-card-grid li { + padding: 5px; + overflow: hidden; + font-size: 11px; + text-align: center; + + width: 100px; + height: 105px; + + box-sizing: border-box; +} + +.umb-card-grid li.-four-in-row { + flex: 0 0 25%; +} + +.umb-card-grid li.-three-in-row { + flex: 0 0 33.33%; +} + +.umb-card-grid .umb-card-grid-item { + display: block; + width: 100%; + height: 100%; +} + + + .umb-card-grid .umb-card-grid-item:hover, + .umb-card-grid .umb-card-grid-item:focus, + .umb-card-grid .umb-card-grid-item:hover > *, + .umb-card-grid .umb-card-grid-item:focus > * { + background: @blue; + color: white; + cursor: pointer; + outline: none; +} + +.umb-card-grid a { + color: #222; + text-decoration: none; + } + +.umb-card-grid i { + font-size: 30px; + line-height: 50px; + display: block; + color: @grayDark; + } + + + + + +//Round icon-like button - this should be somewhere else +.umb-btn-round{ + padding: 4px 6px 4px 6px; + display: inline-block; + cursor: pointer; + border-radius: 200px; + background: rgba(255,255,255, 1); + border:1px solid rgb(182, 182, 182); + margin: 2px; +} + +.umb-btn-round:hover, .umb-btn-round:hover *{ + background: @blue !important; + color: white !important; + border-color: @blue !important; + text-decoration:none; + } + + .umb-btn-round a:hover { + text-decoration:none; + color: white !important; + } + + .umb-btn-round i { + font-size:16px !important; + color: #grayLight; + display:block; + } + + .umb-btn-round.alert:hover, .umb-btn-round.alert:hover *{ + background: @red !important; + color: white !important; + border-color: @red !important; + text-decoration:none; + } + +.umb-btn-round.no-border{ + border: none !important; + background: none !important; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less new file mode 100644 index 0000000000..6d43286f2d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -0,0 +1,63 @@ +/* + contains styling for all main editor directives +*/ + +.umb-editor-wrapper{ + background: white; + position: absolute; + top: 0px; bottom: 0px; left: 0px; right: 0px; +} + + +.umb-editor-header{ + background: @grayLighter; + border-bottom: 1px solid @grayLight; + position: absolute; + height: 99px; + top: 0px; + right: 0px; + left: 0px; +} + + +.umb-editor-container { + top: 101px; + left: 0px; + right: 0px; + bottom: 52px; + position: absolute; + clear: both; + overflow: auto; +} + +.umb-editor-wrapper.-no-footer .umb-editor-container { + bottom: 0; +} + +.umb-editor-container.-stop-scrolling { + overflow: hidden; +} + +.umb-editor-drawer{ + margin: 0; + padding: 10px 20px; + z-index: 999; + position: absolute; + bottom: 0px; + left: 0px; + right: 0px; + height: 31px; + + background: @grayLighter; + border-top: 1px solid @grayLight; +} + + +.umb-editor-actions{ + list-style: none; + margin: 0; padding: 0; +} + +.umb-editor-actions li{ + display: inline-block; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less new file mode 100644 index 0000000000..b7d6015b76 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -0,0 +1,59 @@ +.umb-editor-sub-header { + padding: 15px 0; + margin-bottom: 30px; + background: #ffffff; + display: flex; + justify-content: space-between; + margin-top: -30px; + position: relative; + top: 0; +} + +.umb-editor-sub-header.-umb-sticky-bar { + box-shadow: 0 5px 0 rgba(0, 0, 0, 0.08), 0 1px 0 rgba(0, 0, 0, 0.16); + transition: box-shadow 1s; + top: 101px; /* height of header: 100px + its bottom-border: 1px */ + transform: translate(0, 50%); + +} + +.umb-group-builder__property-preview .umb-editor-sub-header { + display: none; +} + +.umb-editor-sub-header__content-left { + margin-right: auto; +} + +.umb-editor-sub-header__content-right { + margin-left: auto; +} + +.umb-editor-sub-header__content-left, +.umb-editor-sub-header__content-right { + display: flex; + align-items: stretch; +} + +.umb-editor-sub-header__section { + border-left: 1px solid @grayLight; + display: flex; + align-items: center; + padding-left: 20px; + padding-right: 20px; +} + +.umb-editor-sub-header__content-left .umb-editor-sub-header__section:first-child { + border-left: none; + padding-left: 0; +} + +.umb-editor-sub-header__content-right .umb-editor-sub-header__section { + border-left: none; + border-right: 1px solid @grayLight; +} + +.umb-editor-sub-header__content-right .umb-editor-sub-header__section:last-child { + border-right: none; + padding-right: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less new file mode 100644 index 0000000000..1bd4ecf1df --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less @@ -0,0 +1,35 @@ +.umb-notifications { + z-index: 1000; + position: absolute; + bottom: 52px; + left: 0; + right: 0; + border-bottom: none; + margin: auto; + padding: 0px; + border: none; + background: none; + border-radius: 0; +} + +.umb-notifications__notifications { + list-style: none; + margin: 0; + position: relative; +} + + +.umb-notifications__notification { + padding: 5px 20px; + text-shadow: none; + font-size: 12px; + border: none; + border-radius: 0; + position: relative; + margin-bottom: 0; +} + +.umb-notifications__notification.-extra-padding { + padding-top: 20px; + padding-bottom: 20px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less new file mode 100644 index 0000000000..0af0555b89 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -0,0 +1,207 @@ +.umb-overlay { + position: fixed; + overflow: hidden; + background: white; + z-index: 996660; + animation: fadeIn 0.2s; + box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); +} + +.umb-overlay__form { + display: flex; + flex-wrap: nowrap; + flex-direction: column; + height: 100%; +} + +.umb-overlay .umb-overlay-header { + background: @grayLighter; + border-bottom: 1px solid @grayLight; + padding: 10px; + margin-top: 0; + flex-grow: 0; + flex-shrink: 0; +} + + +.umb-overlay .umb-overlay__title { + font-size: @fontSizeLarge; + color: @black; + font-weight: bold; + + margin: 7px 0; +} + +.umb-overlay .umb-overlay__subtitle { + font-size: @fontSizeSmall; + color: @gray; +} + +.umb-overlay .umb-overlay-container { + flex-grow: 1; + flex-shrink: 1; + flex-basis: auto; + overflow-y: auto; + overflow-x: hidden; + position: relative; + height: auto; +} + +.umb-overlay .umb-overlay-drawer { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 31px; + padding: 10px 20px; + margin: 0; + + background: @grayLighter; + border-top: 1px solid @grayLight; +} + +.umb-overlay .umb-overlay-drawer.-auto-height { + flex-basis: auto; +} + +.umb-overlay .umb-overlay-drawer .umb-overlay-drawer__align-right { + display: flex; + justify-content: flex-end; +} + +.umb-overlay .umb-overlay-drawer .umb-overlay-drawer-content .dropdown-menu { + right: 0; + left: auto; +} + +/* ---------- OVERLAY CENTER ---------- */ +.umb-overlay.umb-overlay-center { + position: absolute; + width: 600px; + height: 500px; + top: 10px; + left: 50%; + transform: translate(-50%, 0); +} + +.umb-overlay.umb-overlay-center .umb-overlay-header { + text-align: center; +} + +.umb-overlay.umb-overlay-center .umb-overlay-container { + padding: 20px; +} + +/* ---------- OVERLAY TARGET ---------- */ +.umb-overlay.umb-overlay-target { + width: 400px; + height: 400px; + box-sizing: border-box; +} + +.umb-overlay.umb-overlay-target .umb-overlay-header { + text-align: center; +} + +.umb-overlay.umb-overlay-target .umb-overlay-container { + padding: 10px; +} + +/* ---------- OVERLAY RIGHT ---------- */ +.umb-overlay.umb-overlay-right { + width: 500px; + top: 0; + right: 0; + bottom: 0; + border: none; + box-shadow: 0 0 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); +} + +.umb-overlay.umb-overlay-right .umb-overlay-header { + flex-basis: 100px; + padding: 20px; + box-sizing: border-box; +} + +.umb-overlay.umb-overlay-right .umb-overlay-container { + padding: 20px; +} + +/* ---------- OVERLAY LEFT ---------- */ +.umb-overlay.umb-overlay-left { + width: 500px; + top: 0; + left: 0; + bottom: 0; + border: none; + box-shadow: 0 0 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); + margin-left: 80px; +} + +.umb-overlay.umb-overlay-left .umb-overlay-header { + flex-basis: 100px; + padding: 20px; + box-sizing: border-box; +} + +.umb-overlay.umb-overlay-left .umb-overlay-container { + padding: 20px; +} + +@media (max-width: 767px) { + .umb-overlay.umb-overlay-left { + margin-left: 60px; + } +} + +/* ---------- OVERLAY ITEM DETAILS ---------- */ +.umb-overlay__item-details { + position: absolute; + left: 0; + bottom: 51px; + width: 100%; + padding: 20px; + box-sizing: border-box; + border-bottom: 1px solid @grayLight; + background: @grayLighter; +} + +.umb-overlay__item-details-title-wrapper { + display: flex; + flex-direction: row; + align-items: center; +} + +.umb-overlay__item-details-icon { + font-size: 16px; + margin-right: 10px; + vertical-align: middle; + color: #999; +} + +.umb-overlay__item-details-title { + margin-top: 0; + margin-bottom: 0; +} + +.umb-overlay__item-details-description { + margin-top: 10px; + font-size: 12px; +} + +/*ensures dialogs doesnt have side-by-side labels*/ +.umb-overlay .control-label, +.umb-overlay .form-horizontal .control-label, +.form-horizontal .umb-overlay .control-label + { + width: 100%; + display: block; + box-sizing: border-box; + margin-bottom: 10px; + float: none; +} + + +.umb-overlay .controls-row, +.umb-overlay .form-horizontal .controls, +.form-horizontal .umb-overlay .controls { + margin-left: 0 !important; +} 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 new file mode 100644 index 0000000000..a05e9c8f95 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-overlay-backdrop.less @@ -0,0 +1,10 @@ +.umb-overlay-backdrop { + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.50); + z-index: 2000; + top: 0; + left: 0; + animation: fadeIn 0.2s; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tooltip/umb-tooltip-list.less b/src/Umbraco.Web.UI.Client/src/less/components/tooltip/umb-tooltip-list.less new file mode 100644 index 0000000000..4f704be511 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/tooltip/umb-tooltip-list.less @@ -0,0 +1,19 @@ +.umb-tooltip-list { + list-style: none; + margin-left: 0; + margin-bottom: 0; + padding: 10px; +} + +.umb-tooltip-list__item { + margin-bottom: 5px; +} + +.umb-tooltip-list__item:last-child { + margin-bottom: 0; +} + +.umb-tooltip-list__item-label { + font-weight: bold; + margin-bottom: -3px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tooltip/umb-tooltip.less b/src/Umbraco.Web.UI.Client/src/less/components/tooltip/umb-tooltip.less new file mode 100644 index 0000000000..b058cd1b4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/tooltip/umb-tooltip.less @@ -0,0 +1,13 @@ +.umb-tooltip { + position: fixed; + display: flex; + background: #ffffff; + padding: 10px; + z-index: 1000; + max-width: 200px; + font-size: 12px; + animation-duration: 0.1s; + animation-timing-function: ease-in; + animation: fadeIn; + margin-top: 15px; +} 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 new file mode 100644 index 0000000000..5854599722 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -0,0 +1,31 @@ +.umb-breadcrumbs { + list-style: none; + margin-bottom: 0; + margin-left: 0; +} + +.umb-breadcrumbs__ancestor { + display: inline-block; +} + +.umb-breadcrumbs__ancestor-link, +.umb-breadcrumbs__ancestor-text { + font-size: 11px; + color: #555; +} + +.umb-breadcrumbs__ancestor-link { + text-decoration: underline; +} + +.umb-breadcrumbs__ancestor-link:hover { + color: #000; +} + +.umb-breadcrumbs__seperator { + position: relative; + top: 1px; + margin-left: 5px; + margin-right: 5px; + color: #ccc; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less new file mode 100644 index 0000000000..f604b79d04 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less @@ -0,0 +1,68 @@ +.umb-checkbox-list { + list-style: none; + margin-left: 0; +} + +.umb-checkbox-list__item { + display: flex; + align-items: center; + margin-bottom: 2px; +} + +.umb-checkbox-list__item:last-child { + border-bottom: none; +} + +.umb-checkbox-list__item:hover { + background-color: @grayLighter; +} + +.umb-checkbox-list__item.-selected, +.umb-checkbox-list__item.-disabled { + background-color: fade(@blueDark, 4%); + font-weight: bold; +} + +.umb-checkbox-list__item-checkbox { + display: flex; + justify-content: center; + align-items: center; + flex: 0 0 30px; + height: 30px; + margin-right: 10px; +} + +.umb-checkbox-list__item-checkbox.-selected { + background: @blue; +} + +.umb-checkbox-list__item-icon { + margin-right: 5px; +} + +.umb-checkbox-list__item-text { + font-size: 13px; + margin-bottom: 0; + flex: 1 1 auto; +} + +.umb-checkbox-list__item-text.-faded { + opacity: 0.5; +} + +.umb-checkbox-list__item.-disabled .umb-checkbox-list__item-text { + cursor: not-allowed; +} + +.umb-checkbox-list__item-caption { + font-size: 11px; + margin-left: 2px; +} + +.umb-checkbox-list__no-data { + text-align: center; + padding-top: 50px; + color: #ccc; + font-size: 16px; + line-height: 1.8em; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less new file mode 100644 index 0000000000..67b5a0495e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less @@ -0,0 +1,59 @@ +.umb-child-selector__child { + background: @grayLighter; + padding: 5px 15px; + margin-bottom: 5px; + min-width: 300px; + display: flex; + animation: fadeIn 0.5s; +} + +.umb-child-selector__child.-parent { + background: #f1f1f1; + padding-top: 10px; + padding-bottom: 10px; +} + +.umb-child-selector__child.-placeholder { + border: 1px dashed @grayLight; + background: none; + cursor: pointer; + text-align: center; + justify-content: center; +} + +.umb-child-selector__children-container { + margin-left: 30px; +} + +.umb-child-selector__child-description { + flex: 1; +} + +.umb-child-selector__child-icon-holder { + margin-right: 5px; + width: 20px; + text-align: center; + display: inline-block; +} + +.umb-child-selector__child-icon { + font-size: 16px; + vertical-align: middle; +} + +.umb-child-selector__child-name { + font-size: 13px; +} + +.umb-child-selector__child-name.-blue { + color: @blue; +} + +.umb-child-selector__child-actions { + flex: 0 0 50px; + text-align: right; +} + +.umb-child-selector__child-remove { + cursor: pointer; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less new file mode 100644 index 0000000000..1c31b97baa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-confirm-action.less @@ -0,0 +1,130 @@ +// OVERLAY +.umb_confirm-action__overlay { + position: absolute; + z-index: 999999; + display: flex; +} + +// positions +.umb_confirm-action__overlay.-top { + top: -50px; + right: auto; + bottom: auto; + left: 0; + animation: fadeInUp 0.2s; + flex-direction: column; + + .umb_confirm-action__overlay-action { + margin-bottom: 5px; + } + + .umb_confirm-action__overlay-action.-confirm { + order: 1; + } + + .umb_confirm-action__overlay-action.-cancel { + order: 2; + } + +} + +.umb_confirm-action__overlay.-right { + top: 0; + right: -50px; + bottom: auto; + left: auto; + animation: fadeInLeft 0.2s; + flex-direction: row; + + .umb_confirm-action__overlay-action { + margin-left: 5px; + } + + .umb_confirm-action__overlay-action.-confirm { + order: 2; + } + + .umb_confirm-action__overlay-action.-cancel { + order: 1; + } +} + +.umb_confirm-action__overlay.-bottom { + top: auto; + right: auto; + bottom: -50px; + left: 0; + animation: fadeInDown 0.2s; + flex-direction: column; + + .umb_confirm-action__overlay-action { + margin-top: 5px; + } + + .umb_confirm-action__overlay-action.-confirm { + order: 2; + } + + .umb_confirm-action__overlay-action.-cancel { + order: 1; + } +} + +.umb_confirm-action__overlay.-left { + top: 0; + right: auto; + bottom: auto; + left: -50px; + animation: fadeInRight 0.2s; + flex-direction: row; + + .umb_confirm-action__overlay-action { + margin-right: 5px; + } + + .umb_confirm-action__overlay-action.-confirm { + order: 1; + } + + .umb_confirm-action__overlay-action.-cancel { + order: 2; + } +} + +// BUTTONS +.umb_confirm-action__overlay-action { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + border-radius: 40px; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); + font-size: 18px; +} + +.umb_confirm-action__overlay-action:hover { + text-decoration: none; + color: #ffffff; +} + +// confirm button +.umb_confirm-action__overlay-action.-confirm { + background: #ffffff; + color: @green !important; +} + +.umb_confirm-action__overlay-action.-confirm:hover { + color: @green !important; +} + +// cancel button +.umb_confirm-action__overlay-action.-cancel { + background: #ffffff; + color: @red !important; +} + +.umb_confirm-action__overlay-action.-cancel:hover { + color: @red !important; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less new file mode 100644 index 0000000000..aef523887c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -0,0 +1,114 @@ +.umb-content-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; +} + +.umb-content-grid__item { + background: @grayLighter; + flex: 0 1 200px; + cursor: pointer; + position: relative; + margin: 10px; + user-select: none; +} + +.umb-content-grid__item.-selected { + box-shadow: 0 2px 8px 0 rgba(0,0,0,0.35); +} + +.umb-content-grid__icon-container { + background: lighten(@grayLight, 7%); + height: 75px; + display: flex; + align-items: center; + justify-content: center; +} + +.umb-content-grid__icon, +.umb-content-grid__icon[class^="icon-"], +.umb-content-grid__icon[class*=" icon-"] { + font-size: 40px; + color: lighten(@gray, 20%); +} + +.umb-content-grid__icon.-light { + color: @grayLight; +} + + +.umb-content-grid__content { + box-sizing: border-box; + padding: 15px; +} + +.umb-content-grid__item-name { + font-weight: bold; + margin-bottom: 10px; + color: black; + padding-bottom: 10px; + line-height: 1.4em; + border-bottom: 1px solid @grayLight; + display: inline-block; +} + +.umb-content-grid__item-name:hover { + text-decoration: underline; +} + +.umb-content-grid__item-name.-light { + color: @grayLight; +} + +.umb-content-grid__details-list { + list-style: none; + margin-bottom: 0; + margin-left: 0; + font-size: 11px; +} + +.umb-content-grid__details-list.-light { + color: @grayLight; +} + +.umb-content-grid__details-label { + font-weight: bold; + display: inline-block; +} + +.umb-content-grid__details-value { + display: inline-block; +} + +.umb-content-grid__checkmark { + position: absolute; + top: 10px; + right: 10px; + border: 1px solid #ffffff; + width: 25px; + height: 25px; + background: @blue; + border-radius: 50px; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + color: #ffffff; + cursor: pointer; +} + +.umb-content-grid__item:hover .umb-content-grid__action:not(.-selected) { + opacity: 1; + animation: fadeIn; + animation-duration: 0.2s; + animation-timing-function: ease-in-out; +} + +.umb-content-grid__no-items { + font-size: 16px; + font-weight: bold; + color: @grayLight; + padding-top: 50px; + padding-bottom: 50px; +} 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 new file mode 100644 index 0000000000..b5e7cf7bed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less @@ -0,0 +1,39 @@ +.umb-sub-views-nav { + list-style: none; + display: flex; + margin: 0; +} + +.umb-sub-views-nav-item { + text-align: center; + margin-left: 20px; + cursor: pointer; + display: block; +} + +.umb-sub-views-nav-item:focus { + outline: none; +} + +.umb-sub-views-nav-item:hover, +.umb-sub-views-nav-item:focus { + text-decoration: none; +} + +.umb-sub-views-nav-item.is-active { + color: @blue; +} + +.show-validation .umb-sub-views-nav-item.-has-error { + color: @red; +} + +.umb-sub-views-nav-item .icon { + font-size: 24px; + display: block; + text-align: center; +} + +.umb-sub-views-nav-item-text { + font-size: 11px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-sub-views.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-sub-views.less new file mode 100644 index 0000000000..ac034cf33b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-sub-views.less @@ -0,0 +1,23 @@ +/* --------- COLUMNS --------- */ + +.sub-view-columns { + display: flex; + margin-bottom: 40px; +} + +.sub-view-columns h5 { + margin-top: 0; +} + +.sub-view-column-left { + flex: 0 0 250px; + margin-right: 70px; +} + +.sub-view-column-right { + flex: 1; +} + +.sub-view-column-section { + margin-bottom: 20px; +} \ No newline at end of file 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 new file mode 100644 index 0000000000..9ed61bcb39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-empty-state.less @@ -0,0 +1,23 @@ +.umb-empty-state { + font-size: @fontSizeMedium; + line-height: 1.8em; + color: @grayMed; + text-align: center; +} + +.umb-empty-state.-small { + font-size: @fontSizeSmall; +} + +.umb-empty-state.-large { + font-size: @fontSizeLarge; +} + +.umb-empty-state.-center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + width: 80%; + max-width: 400px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less new file mode 100644 index 0000000000..6b3c46d927 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less @@ -0,0 +1,129 @@ + +.umb-file-dropzone-directive{ + + // drop zone + // tall and small version - animate height + .dropzone { + height: 400px; + width: auto; + padding: 50px 0; + border: 1px dashed @grayLight; + text-align: center; + color: @gray; + margin: 0 0 20px 0; + position: relative; + -webkit-transition: height 0.8s; + -moz-transition: height 0.8s; + transition: height 0.8s; + .illustration { + width: 300px; + } + &.is-small { + height: 100px; + .illustration { + width: 200px; + } + } + &.drag-over { + border: 1px dashed @grayDark; + } + } + + + // center the content of the drop zone + .content { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%,-50%); + } + + + // file select link + .file-select { + font-size: 15px; + color: @blue; + cursor: pointer; + + margin-top: 10px; + + &:hover { + color: @blueDark; + text-decoration: underline; + } + } + + // uploading / uploaded file list + .file-list { + list-style: none; + margin: 0 0 30px 0; + background: @grayLighter; + padding: 10px 20px; + + .file { + //border-bottom: 1px dashed @orange; + display: block; + width: 100%; + padding: 5px 0; + position: relative; + border-top: 1px solid @grayLight; + &:first-child { + border-top: none; + } + + &.ng-enter { + animation: fadeIn 0.5s; + } + &.ng-leave { + animation: fadeOut 2s; + } + .file-description { + color: @grayDarker; + font-size: 12px; + width: 100%; + display: block; + } + + .file-upload-progress { + display: block; + width: 100%; + } + + .file-icon { + position: absolute; + right: 0; + bottom: 0; + + .icon { + font-size: 20px; + &.ng-enter { + animation: fadeIn 0.5s; + } + &.ng-leave { + animation: fadeIn 0.5s; + } + } + } + } + } + + + // progress bars + // could be moved to its own less file + .file-progress { + height: 4px; + position: relative; + padding: 2px; + + .file-progress-indicator { + display: block; + height: 100%; + border-radius: 20px; + background-color: @blue; + position: relative; + overflow: hidden; + width: 0; + } + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-folder-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-folder-grid.less new file mode 100644 index 0000000000..c7c7fb78e5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-folder-grid.less @@ -0,0 +1,77 @@ +.umb-folder-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + margin-bottom: 30px; +} + +.umb-folder-grid__folder { + background: @grayLighter; + margin: 5px; + display: flex; + align-items: center; + padding: 10px 20px; + box-sizing: border-box; + flex: 1 1 200px; + border: 1px solid transparent; + transition: border 0.2s; + position: relative; + justify-content: space-between; + cursor: pointer; + user-select: none; +} + +.umb-folder-grid__folder:focus, +.umb-folder-grid__folder:active { + text-decoration: none; +} + +.umb-folder-grid__folder:hover { + text-decoration: none; + background-color: darken(@grayLighter, 5%); +} + +.umb-folder-grid__folder-description { + display: flex; + align-items: center; +} + +.umb-folder-grid__folder-icon, +.umb-folder-grid__folder-icon[class^="icon-"], +.umb-folder-grid__folder-icon[class*=" icon-"] { + font-size: 20px; + margin-right: 15px; + color: @black; +} + +.umb-folder-grid__folder-name { + font-size: 13px; + color: @black; + font-weight: bold; +} + +.umb-folder-grid__folder-name:hover { + text-decoration: underline; +} + +.umb-folder-grid__action { + opacity: 0; + border: 1px solid #ffffff; + width: 25px; + height: 25px; + background: rgba(0, 0, 0, 0.4); + border-radius: 50px; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + color: #ffffff; + margin-left: 7px; + cursor: pointer; +} + +.umb-folder-grid__action.-selected { + opacity: 1; + background: @blue; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less new file mode 100644 index 0000000000..d05bc9fc43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid-selector.less @@ -0,0 +1,74 @@ +.umb-grid-selector__items { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.umb-grid-selector__item { + width: 125px; + height: 150px; + padding: 20px; + background: #f8f8f8; + border: 1px solid #CCCCCC; + text-align: center; + margin: 0 20px 20px 0; + display: flex; + align-items: center; + justify-content: center; + animation: fadeIn 0.5s; + position: relative; +} + +.umb-grid-selector__item.-default { + border-width: 2px; +} + +.umb-grid-selector__item.-placeholder { + border: 1px dashed #d9d9d9; + background: none; + cursor: pointer; +} + +.umb-grid-selector__item-content { + margin-top: 10px; +} + +.umb-grid-selector__item-icon { + font-size: 50px; + color: #d9d9d9; + display: block; + line-height: 50px; + margin-bottom: 5px; +} + +.umb-grid-selector__item-label { + font-size: 13px; + font-weight: bold; +} + +.umb-grid-selector__item-label.-blue { + color: @blue; +} + +.umb-grid-selector__item-remove { + position: absolute; + top: 5px; + right: 5px; + cursor: pointer; +} + +.umb-grid-selector__item-default-label { + font-size: 10px; + color: @gray; + font-weight: bold; + position: relative; + top: -3px; +} + +.umb-grid-selector__item-default-label.-blue { + color: @blue; +} + +.umb-grid-selector__item-add { + color: @blue; +} 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 new file mode 100644 index 0000000000..4b63bfb1a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -0,0 +1,1097 @@ +// TODO General cleanup in !important (Round 2) + +// Gridview +// ------------------------- +.umb-grid IFRAME { + overflow: hidden; +} + + + +// Sortabel +// ------------------------- + +// sortable-helper +.umb-grid .ui-sortable-helper { + position: absolute !important; + background-color: @blue !important; + height: 42px !important; + width: 42px !important; + overflow: hidden; + padding: 5px; + border-radius: 2px; + text-align: center; + font-family: "icomoon"; + box-shadow: 3px 3px 12px 0 rgba(50, 50, 50, 0.45); + + &:after { + line-height: 42px; + font-size: 22px; + content: "\e126"; + color: #ffffff; + } + + * { + display: none; + } +} + +.umb-grid .ui-sortable-helper .umb-row-title-bar, +.umb-grid .ui-sortable-helper .cell-tools-add { + display: none !important; +} + +// sortable-placeholder +.umb-grid .ui-sortable-placeholder { + position: absolute; + left: 0; + right: 0; + background-color: @blue; + height: 2px; + margin-bottom: 20px; + + &:before, &:after { + position: absolute; + top: -9px; + font-family: "icomoon"; + font-size: 18px; + color: @blue; + } + + &:before { + left: -5px; + content: "\e0e9"; + } + + &:after { + right: -5px; + content: "\e0d7"; + } +} + +.umb-grid .umb-cell .ui-sortable-placeholder { + left: 10px; + right: 10px; +} + + + + +// utils +// ------------------------- +.umb-grid-width { + margin: 20px auto; + width: 100%; +} + +.umb-grid .right { + float: right; +} + + + +// general layout components +// ------------------------- +.umb-grid .tb { + width: 100%; +} + +.umb-grid .td { + width: 100%; + display: inline-block; + vertical-align: top; + box-sizing: border-box; +} + +.umb-grid .middle { + text-align: center; +} + +.umb-grid .mainTd { + position: relative; +} + + + +// COLUMN +// ------------------------- +.umb-grid .umb-column { + position: relative; +} + + + +// ROW +// ------------------------- +.umb-grid .umb-row { + position: relative; + margin-bottom: 40px; + padding-top: 10px; +} + +.umb-grid .row-tools a { + text-decoration: none; +} + + + +// CELL +// ------------------------- +.umb-grid .umb-cell { + position: relative; +} + +.umb-grid .umb-cell-content { + position: relative; + display: block; + box-sizing: border-box; + margin: 10px; + border: 1px solid transparent; +} + +.umb-grid .umb-row .umb-cell-placeholder { + min-height: 130px; + background-color: @grayLighter; + border-width: 2px; + border-style: dashed; + border-color: @grayLight; + transition: border-color 100ms linear; + + &:hover { + border-color: @blue; + cursor: pointer; + } +} + +.umb-grid .umb-cell-content.-has-editors { + padding-top: 38px; + background-color: #ffffff; + border-width: 1px; + border-style: solid; + border-color: @grayLight; + + &:hover { + cursor: auto; + } +} + +.umb-grid .umb-cell-content.-has-editors.-collapsed { + padding-top: 10px; +} + + + +.umb-grid .cell-tools { + position: absolute; + top: 10px; + right: 10px; + color: @gray; + font-size: 16px; +} + + +.umb-grid .cell-tool { + cursor: pointer; + float: right; + + &:hover { + color: @blueDark; + } +} + + +.umb-grid .cell-tools-add { + color: @blue; + + &:focus, &:hover { + text-decoration: none; + } +} + +.umb-grid .cell-tools-add.-center { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: @blue; +} + +.umb-grid .cell-tools-add.-bar { + display: block; + background: @grayLighter; + text-align: center; + padding: 5px; + border: 1px dashed #ccc; + margin: 10px; +} + + +.umb-grid .cell-tools-remove { + display: inline-block; + position: relative; +} + +.umb-grid .cell-tools-remove .iconBox:hover, +.umb-grid .cell-tools-remove .iconBox:hover * { + background: @red !important; + border-color: @red !important; +} + +.umb-grid .cell-tools-move { + display: inline-block; +} + +.umb-grid .cell-tools-edit { + display: inline-block; + color: @white; +} + +.umb-grid .drop-overlay { + position: absolute; + z-index: 10; + top: 0; + left: 0; + background: #ffffff; + opacity: 0.9; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + font-size: 14px; + box-sizing: border-box; + text-align: center; + line-height: 1.3em; + font-weight: bold; + flex-direction: column; +} + +.drop-overlay.-disable { + color: @red; +} + +.drop-overlay.-allow { + color: @green; +} + +.umb-grid .drop-overlay .drop-icon { + font-size: 40px; + margin-bottom: 20px; +} + + + +// CONTROL +// ------------------------- +.umb-grid .umb-control { + position: relative; + display: block; + overflow: hidden; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 10px; +} + +.umb-control-collapsed { + background-color: @grayLighter; + padding: 5px 10px; + border-width: 1px; + border-style: solid; + border-color: transparent; + cursor: move; +} + +.umb-control-collapsed:hover { + border-color: @blue; +} + +.umb-grid .umb-control-click-overlay { + position: absolute; + width: 100%; + height: 100%; + z-index: 5; + top: 0; + left: 0; + opacity: 0; + cursor: pointer; + + &:hover { + opacity: 0.1; + background-color: @blue; + transition: opacity 0.1s; + } +} + + +.umb-grid .umb-row-title-bar { + display: flex; + padding-left: 10px; + padding-right: 10px; +} + +.umb-grid .umb-row-title { + display: inline-block; + cursor: pointer; + font-size: 15px; + font-weight: bold; + color: @black; + + margin-right: 6px; +} + +.umb-grid .row-tools { + display: inline-block; + margin-left: 10px; + font-size: 18px; + color: @gray; +} + +.umb-grid .row-tool { + cursor: pointer; +} + +.umb-grid .umb-add-row { + text-align: center; +} + + + +// CONTROL PLACEHOLDER +// ------------------------- +.umb-grid .umb-control-placeholder { + min-height: 20px; + position: relative; + text-align: center; + text-align: -moz-center; + cursor: text; +} + +.umb-grid .umb-control-placeholder .placeholder { + font-size: 14px; + opacity: 0.7; + text-align: left; + padding: 5px; + border: 1px solid rgba(182, 182, 182, 0.3); + height: 20px; +} + +.umb-grid .umb-control-placeholder:hover .placeholder { + border: 1px solid rgba(182, 182, 182, 0.8); +} + + + +// EDITOR PLACEHOLDER +// ------------------------- +.umb-grid .umb-editor-placeholder { + min-height: 65px; + padding: 20px; + padding-bottom: 30px; + position: relative; + background-color: @white; + border: 4px dashed @grayLight; + text-align: center; + text-align: -moz-center; + cursor: pointer; +} + +.umb-grid .umb-editor-placeholder i { + color: @grayLight; + font-size: 85px; + line-height: 85px; + display: block; + margin-bottom: 10px; +} + + + +// Active states +// ------------------------- + +// Row states +.umb-grid .umb-row.-active { + background-color: @blue; + + .umb-row-title-bar { + cursor: move; + } + + .row-tool, + .umb-row-title { + color: #fff; + } + + .umb-grid-has-config { + color: fade(@white, 66); + } + + .umb-cell { + .umb-grid-has-config { + color: fade(@black, 44); + } + } + + .umb-cell .umb-cell-content { + border-color: transparent; + } +} + + +.umb-grid .umb-row.-active-child { + background-color: @grayLighter; + + .umb-row-title-bar { + cursor: default; + } + + .umb-row-title { + color: @gray; + } + + .row-tool { + color: fade(@black, 23); + } + + .umb-grid-has-config { + color: fade(@black, 44); + } + + .umb-cell-content.-placeholder { + border-color: @grayLight; + + &:hover { + border-color: fade(@gray, 44); + } + } +} + + +// Cell states + +.umb-grid .umb-row .umb-cell.-active { + border-color: @grayLight; + + .umb-cell-content.-has-editors { + box-shadow: 3px 3px 6px rgba(0, 0, 0, .07); + border-color: @blue; + } +} + +.umb-grid .umb-row .umb-cell.-active-child { + + .cell-tool { + color: fade(@black, 23); + } + + .umb-cell-content.-has-editors { + border-color: rgba(113, 136, 160, .44); + } +} + + + + + +// Title bar and tools +.umb-grid .umb-row-title-bar { + display: flex; +} + +.umb-grid .umb-grid-right { + display: flex; + flex-direction: row; + justify-content: center; +} + +.umb-grid .umb-tools { + margin-left: auto; +} + + +// Add more content button +.umb-grid-add-more-content { + text-align: center; +} + +.umb-grid .newbtn { + width: auto; + padding: 6px 15px; + border-style: solid; + + font-size: 12px; + font-weight: bold; + + display: inline-block; + + margin: 10px auto 20px; + + border-color: #E2E2E2; + + &:hover { + cursor: pointer; + opacity: .77; + } +} + + + + +// Form elements +// ------------------------- +.umb-grid textarea.textstring { + display: block; + overflow: hidden; + border: none; + background: #fff; + outline: none; + resize: none; + color: @gray; +} + +.umb-grid .umb-cell-rte textarea { + display: none !important; +} + +.umb-grid .umb-cell-media .caption { + display: block; + overflow: hidden; + border: none; + background: #fff; + outline: none; + width: 98%; + resize: none; + font-style: italic; +} + +.umb-grid .cellPanelRte { + min-height: 60px; +} + +.umb-grid .umb-cell-embed iframe { + width: 100%; +} + +.umb-grid .umb-cell-rte { + border-color: transparent; +} + + + +// ICONS +// ------------------------- +.umb-grid .iconBox { + padding: 4px 6px; + display: inline-block; + cursor: pointer; + border-radius: 200px; + background: rgba(255,255,255, 1); + border: 1px solid rgb(182, 182, 182); + margin: 2px; + + &:hover, &:hover * { + background: @blue !important; + color: white !important; + border-color: @blue !important; + text-decoration: none; + } +} + +.umb-grid .iconBox span.prompt { + display: block; + white-space: nowrap; + text-align: center; +} + +.umb-grid .iconBox span.prompt > a { + text-decoration: underline; +} + +.umb-grid .iconBox a:hover { + text-decoration: none; + color: white !important; +} + +.umb-grid .iconBox.selected { + -webkit-appearance: none; + background-image: linear-gradient(to bottom,#e6e6e6,#bfbfbf); + background-repeat: repeat-x; + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe6e6e6',endColorstr='#ffbfbfbf',GradientType=0); + zoom: 1; + border-color: #bfbfbf #bfbfbf #999; + border-color: rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25); + box-shadow: inset 0 2px 4px rgba(0,0,0,0.15),0 1px 2px rgba(0,0,0,0.05); + border-radius: 3px; + background: transparent; +} + +.umb-grid .iconBox i { + font-size: 16px !important; + color: #5F5F5F; + display: block; +} + +.umb-grid .help-text { + color: @black; + font-size: 14px; + font-weight: bold; + display: inline-block; + clear: both; +} + + + +// TINYMCE EDITOR +// ------------------------- + +.umb-grid .mce-panel { + background: transparent !important; + border: none !important; + clear: both; +} + +.umb-grid .mce-btn button { + padding: 8px 6px; + line-height: inherit; +} + +.umb-grid .mce-toolbar { + border-bottom: 1px solid rgba(207, 207, 207, 0.7); + background-color: rgba(250, 250, 250, 1); + display: none; +} + +.umb-grid .umb-control.-active .mce-toolbar { + display: block; +} + +.umb-grid .mce-flow-layout-item { + margin: 0; +} + +.umb-grid .mceContentBody { + overflow-y: hidden!important; +} + + +// MEDIA EDITOR +// ------------------------- +.umb-grid .fullSizeImage { + width: 100%; +} + + + +// Width +// ------------------------- +.umb-grid .boxWidth { + text-align: right; + margin-bottom: 10px; +} + +.umb-grid .boxWidth input { + text-align: center; + width: 40px; +} + +.umb-grid .boxWidth label { + font-size: 10px; + padding: 0; + margin: 5px 5px 0 0; + color: #808080; +} + + + +// Margin Control +// ------------------------- +.umb-grid .umb-control { + border-width: 1px; + border-style: solid; + border-color: transparent; +} + +.umb-grid .umb-control.-active { + border-color: @blue; +} + +.umb-grid .umb-templates-columns { + margin-top: 30px; +} + +.umb-grid .umb-control-inner { + position: relative; +} + +.umb-grid .umb-control-bar { + opacity: 0; + background: @blue; + padding: 2px 5px; + color: #ffffff; + font-size: 10px; + height: 0; + display: flex; + transition: height 80ms linear, opacity 80ms linear; + align-items: center; +} + +.umb-grid .umb-control-title { + display: flex; + align-items: center; +} + +.umb-grid .umb-control.-active .umb-control-bar { + opacity: 1; + height: 25px; + cursor: move; +} + +.umb-grid .umb-control-tools { + display: inline-block; + margin-left: 10px; +} + +.umb-grid .umb-control-tool { + font-size: 16px; + margin-right: 5px; + position: relative; + cursor: pointer; + display:inline-block; +} + + + +// Template +// ------------------------- +.umb-grid .umb-templates { + text-align: center; + overflow: hidden; + width: 100%; +} + +.umb-grid .umb-templates-template { + display: inline-block; + width: 100px; + padding-right: 30px; + margin: 20px; +} + +.umb-grid .umb-templates-template a.tb:hover { + border: 5px solid @blue; +} + +.umb-grid .umb-templates-template .tb { + width: 100%; + height: 150px; + padding: 10px; + background-color: @grayLighter; + border: 5px solid @grayLight; + cursor: pointer; + position: relative; +} + +.umb-grid .umb-templates-template .tr { + height: 100%; + position: relative; +} + +.umb-grid .umb-templates-template .tb .umb-templates-column { + height: 100%; + border: 1px dashed @grayLight; + border-right: none; +} + +.umb-grid .umb-templates-template .tb .umb-templates-column.last { + border-right: 1px dashed @grayLight !important; +} + +.umb-grid a.umb-templates-column:hover, +.umb-grid a.umb-templates-column.selected { + background-color: @blue; +} + + + +// Template Column +// ------------------------- +/* New template preview */ +.umb-grid { + .templates-preview { + display: inline-block; + width: 100%; + text-align: center; + + small { + position: absolute; + width: 100%; + left: 0; + bottom: -25px; + padding-top: 15px; + } + + .help-text { + margin: 35px 35px 0 0; + } + } + + .preview-rows { + display: inline-block; + position: relative; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 125px; + margin: 15px; + border: 3px solid @grayLight; + transition: border 100ms linear; + + &.prevalues-rows { + margin: 0 20px 20px 0; + width: 80px; + float: left; + } + + &.prevalues-templates { + margin: 0 20px 20px 0; + float: left; + } + + &:hover { + border-color: @blue; + cursor: pointer; + } + + .preview-row { + display: inline-block; + width: 100%; + vertical-align: bottom; + } + } + + .preview-rows.layout { + padding: 2px; + + .preview-row { + height: 100%; + } + + .preview-col { + height: 180px; + } + + .preview-cell { + background-color: @grayLighter; + } + + .preview-overlay { + display: none; + } + } + + .preview-rows.columns { + min-height: 16px; + line-height: 11px; + padding: 1px; + + &.prevalues-rows { + min-height: 30px; + } + } + + .preview-rows { + .preview-col { + display: block; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 33.3%; + + /* temp value */ + height: 10px; + margin: 0; + border: 1px solid @white; + + .preview-cell { + display: block; + width: 100%; + height: 100%; + background-color: @grayLight; + margin: 0 1px 1px 0; + } + } + + &.prevalues-templates { + .preview-col { + height: 80px; + } + } + } + + .preview-overlay { + display: block; + width: 100%; + position: absolute; + height: 100%; + top: 0; + box-sizing: border-box; + left: 0; + border: 3px solid white; + } +} + +// Has Config +// ------------------------- + +.umb-grid .umb-grid-has-config { + display: inline; + + font-size: 12px; + color: fade(@black, 44); +} + +.umb-grid .umb-cell { + .umb-grid-has-config { + position: absolute; + top: 10px; + left: 10px; + } +} + + +// Overlay +// ------------------------- +.umb-grid .cell-tools-menu { + position: absolute; + width: 360px; + height: 380px; + overflow: auto; + border: 1px solid #ccc; + margin-top: -270px; + margin-left: -150px; + background: @white; + padding: 7px; + top: 0; + left: 50%; + z-index: 6660; + box-shadow: 3px 3px 12px 0 rgba(50, 50, 50, 0.45); +} + +.umb-grid .cell-tools-menu h5 { + border-bottom: 1px solid #d9d9d9; + color: #999; + padding: 10px; + margin-top: 0; +} + +.umb-grid .elements { + display: block; + padding: 0; + margin: 0; +} + +.umb-grid .elements li { + display: inline-block; + width: 90px; + height: 80px; + margin: 5px; + padding: 5px; + overflow: hidden; + font-size: 11px; + + &:hover, &:hover * { + background: #2e8aea; + color: @white; + } +} + +.umb-grid .elements a { + color: #222; + text-decoration: none; +} + +.umb-grid .elements i { + font-size: 30px; + line-height: 50px; + color: #999; + display: block; +} + + + +// Configuration specific styles +// ------------------------- +.umb-grid-configuration .umb-templates { + text-align: left; +} + +.umb-grid-configuration ul { + display: block; +} + +.umb-grid-configuration ul li { + display: block; + width: auto; + text-align: left; +} + +.umb-grid-configuration .umb-templates .umb-templates-template .tb { + max-height: 50px; + border-width: 2px !important; + padding: 0; + border-spacing: 2px; + overflow: hidden; +} + +.umb-grid-configuration .umb-templates .umb-templates-template span { + background: @grayLight; + display: inline-block; +} + +.umb-grid-configuration .umb-templates .umb-templates-template .tb:hover { + border-width: 2px !important; +} + +.umb-grid-configuration .umb-templates-column { + display: block; + float: left; + margin-left: -1px; + border: 1px white solid !important; + background: @grayLight; +} + +.umb-grid-configuration .umb-templates-column.last { + margin-right: -1px; +} + +.umb-grid-configuration .umb-templates-column.add { + text-align: center; + font-size: 20px; + line-height: 70px; + color: #ccc; + text-decoration: none; + background: @white; +} + +.umb-grid-configuration .mainTdpt { + height: initial; + border: none; +} + +.umb-grid-configuration .umb-templates-rows .umb-templates-row { + margin: 0 50px 20px 0; + width: 60px; +} + +.umb-grid-configuration .umb-templates-rows .umb-templates-row .tb { + border-width: 2px !important; + padding: 0; + border-spacing: 2px; +} + +.umb-grid-configuration .umb-templates-rows .mainTdpt { + height: 10px !important; +} + +.umb-grid-configuration a.umb-templates-column { + height: 70px !important; +} 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 new file mode 100644 index 0000000000..46a74f7d8b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -0,0 +1,536 @@ +/* ---------- GROUPS ---------- */ + +.umb-group-builder__groups { + list-style: none; + margin: 0; + padding: 0; +} + +.umb-group-builder__group { + min-height: 86px; + margin: 50px 0 0 0; + border: 2px solid #CCCCCC; + border-radius: 0 10px 10px 10px; + position: relative; + padding: 10px 10px 5px 10px; + box-sizing: border-box; +} + +.umb-group-builder__group.-active { + border-color: @blue; +} + +.umb-group-builder__group.-inherited { + border-color: #F2F2F2; + animation: fadeIn 0.5s; +} + +.umb-group-builder__group.-placeholder { + min-height: 86px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + border: 1px dashed @grayLight; + color: @blue; + font-weight: bold; +} + +.umb-group-builder__group.-sortable { + min-height: initial; + cursor: move; +} + +.umb-group-builder__group-actions { + position: absolute; + top: 5px; + right: 5px; + visibility: hidden; + opacity: 0; + z-index: 10; +} + +.umb-group-builder__group-action { + display: inline-block; +} + +.umb-group-builder__group-remove { + position: absolute; + top: -30px; + right: 20px; + font-size: 18px; +} + +.umb-group-builder__group-remove:hover { + cursor: pointer; + color: @blueDark; +} + +.umb-group-builder__group-title-wrapper { + position: absolute; + left: -2px; + top: -45px; + display: flex; + align-items: center; +} + +.umb-group-builder__group-title-wrapper.-placeholder { + left: -1px; + top: -44px; +} + +.umb-group-builder__group-title { + padding: 5px 9px 0 9px; + height: 38px; + background: white; + border: 2px solid #CCCCCC; + border-bottom: none; + border-radius: 10px 10px 0 0; + font-weight: bold; + display: flex; + align-items: center; +} + +.umb-group-builder__group-title-icon { + margin-left: 5px; +} + +.umb-group-builder__group-title.-active { + border-color: @blue; +} + +.umb-group-builder__group-title.-placeholder { + border: 1px dashed @grayLight; + border-bottom: none; + width: 70px; +} + +.umb-group-builder__group-title.-inherited { + border-color: #F2F2F2; +} + +input.umb-group-builder__group-title-input { + border-color: transparent; + background: transparent; + font-weight: bold; + color: #515151; + margin-bottom: 0; +} + +.umb-group-builder__group-title-input:hover { + border-color: @inputBorder; +} + +.umb-group-builder__group-title-input.-placeholder { + border: 1px dashed #979797; +} + +.umb-group-builder__group-inherited-label { + font-size: 13px; + color: @grayLight; + display: inline-block; + position: relative; + top: 2px; + margin-left: 10px; +} + +.umb-group-builder__group-sort-order { + display: flex; + align-items: center; + margin-left: 10px; + position: relative; + top: 2px; +} + +input.umb-group-builder__group-sort-value { + font-size: 11px; + padding: 0px 0 0px 5px; + width: 40px; + margin-bottom: 0; + border-color: #d9d9d9; +} + +/* ---------- PROPERTIES ---------- */ + +.umb-group-builder__properties { + list-style: none; + margin: 0; + padding: 0; + min-height: 35px; // the height of a sortable property +} + +.umb-group-builder__property { + position: relative; + display: flex; + flex-flow: row; + margin-bottom: 5px; + border: 1px solid transparent; + border-radius: 5px; + padding: 5px; + box-sizing: border-box; +} + +.umb-group-builder__property:hover { + border: 1px dashed @inputBorder; +} + +.umb-group-builder__property.-placeholder { + background: #ffffff; + border: 1px dashed @grayLight; + border-radius: 5px; + cursor: pointer; + align-items: center; + animation: fadeIn 0.5s; +} + +.umb-group-builder__property.-inherited { + border: transparent; + animation: fadeIn 0.5s; +} + +.umb-group-builder__property.-inherited:hover { + border: transparent; +} + +.umb-group-builder__property.-locked { + border: transparent; + animation: fadeIn 0.5s; +} + +.umb-group-builder__property.-locked:hover { + border: transparent; +} + +.umb-group-builder__property.-sortable, +.umb-group-builder__property.-sortable-locked { + min-height: 35px; + border-radius: 5px; + border: none; + animation: none; + align-items: center; +} + +.umb-group-builder__property.-sortable { + background: #E9E9E9; + color: @grayDarker; + cursor: move; +} + +.umb-group-builder__property.-sortable-locked { + background: @grayLighter; + padding-left: 30px; +} + + +.umb-group-builder__property-meta { + flex: 0 0 175px; + margin-right: 20px; +} + +.umb-group-builder__property-meta.-full-width { + flex: 1; +} + +.umb-group-builder__property-meta-alias { + font-size: 10px; + color: @gray; + word-break: break-word; + line-height: 1.5; + margin-bottom: 5px; +} + +.umb-group-builder__property-meta-label textarea { + font-size: 14px; + font-weight: bold; + margin-bottom: 0; + color: @grayDarker; + width: 100%; + padding: 0; + min-height: 25px; + box-sizing: border-box; + resize: none; + overflow: hidden; + border-color: transparent; + background: transparent; +} + +.umb-group-builder__property-meta-label textarea.ng-invalid { + border: none; +} + +.umb-group-builder__property-meta-description textarea { + font-size: 12px; + line-height: 1.5; + color: @grayDark; + margin-bottom: 0; + padding: 0; + width: 100%; + min-height: 25px; + box-sizing: border-box; + resize: none; + overflow: hidden; + border-color: transparent; + background: transparent; +} + +.umb-group-builder__property-preview { + flex: 1; + height: 30px; + overflow: hidden; + position: relative; + padding: 35px 10px 25px 10px; + background: url("../img/checkered-background-20.png"); +} + +.umb-group-builder__property-preview:hover { + cursor: pointer; +} + +.umb-group-builder__property-preview:focus { + outline: none; +} + +.umb-group-builder__property-preview.-not-clickable:hover { + cursor: auto; +} + +.umb-group-builder__property-preview .help-inline { + display:none !important; +} + +.umb-group-builder__property-preview-label { + font-size: 11px; + position: absolute; + top: 0; + left: 0; + text-transform: uppercase; + z-index: 15; + background: @grayLighter; + padding: 3px; + line-height: 12px; + opacity: 0.8 +} + +.umb-group-builder__property-actions { + flex: 0 0 40px; + text-align: center; + margin: 15px 0 0 15px; +} + +.umb-group-builder__property-action { + margin: 0 0 10px 0; + display: block; + font-size: 18px; + position: relative; + cursor: pointer; +} + +.umb-group-builder__property-action:hover { + color: @blueDark; +} + +.umb-group-builder__property-tags { + position: absolute; + z-index: 20; + top: 0; + left: 0; + display: flex; + flex-direction: row; +} + +.umb-group-builder__property-tags.-right { + right: 0; + left: auto; +} + +.umb-group-builder__property-tag { + font-size: 11px; + background-color: @grayLight; + margin-left: 10px; + padding: 0 4px; + display: flex; +} + +.umb-group-builder__property-tag:first-child { + margin-left: 0; +} + +.umb-group-builder__property-tag.-white { + background-color: @white; +} + +.umb-group-builder__property-tag-icon { + margin-right: 3px; +} + + +/* ---------- PLACEHOLDER BOX ---------- */ + +.umb-group-builder__placeholder-box { + background: #E9E9E9; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: bold; + color: @blue; +} + +.umb-group-builder__placeholder-box.-input { + height: 10px; + margin-bottom: 5px; +} + +.umb-group-builder__placeholder-box.-input-small { + height: 5px; + margin-bottom: 5px; + width: 25%; +} + +.umb-group-builder__placeholder-box.-text-full-width { + height: 3px; + margin-bottom: 3px; +} + +.umb-group-builder__placeholder-box.-text-short { + height: 3px; + margin-bottom: 3px; + width: 75%; +} + +/* ---------- SORTABLE ---------- */ + +.umb-group-builder__group-sortable-placeholder { + background: transparent; + border: 1px dashed @grayLight; + margin: 0 0 70px 0; + border-radius: 10px; + border-radius: 5px; +} + +.umb-group-builder__property_sortable-placeholder { + background: transparent; + border: 1px dashed @grayLight; + margin-bottom: 5px; + border-radius: 5px; +} + +.umb-group-builder__no-data-text { + padding-top: 50px; + font-size: 16px; + line-height: 1.8em; + color: #ccc; + text-align: center; +} + + +/* ---------- DIALOGS ---------- */ + +.umb-overlay .show-validation .ng-invalid-val-required-component .editor-placeholder { + border-color: @red; + color: @red; +} + +.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; + } + } + + .editor-placeholder { + border: 1px dashed @grayLight; + width: 100%; + height: 80px; + line-height: 80px; + text-align: center; + display: block; + border-radius: 5px; + color: @gray; + font-weight: bold; + font-size: 13px; + color: @blue; + &:hover { + text-decoration: none; + } + } + + .editor { + margin-bottom: 10px; + .editor-icon-wrapper { + border: 1px solid @grayLight; + 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%; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less new file mode 100644 index 0000000000..e0cdb7cac5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less @@ -0,0 +1,46 @@ +.umb-iconpicker { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + margin: 0; +} + +.umb-iconpicker-item { + display: flex; + flex-direction: row; + flex: 0 0 14.28%; + justify-content: center; + align-items: center; + + margin-bottom: 0; + + overflow: hidden; +} + +.umb-iconpicker-item a { + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; + + padding: 15px 0; + + text-decoration: none; +} + +.umb-iconpicker-item a:hover, +.umb-iconpicker-item a:focus{ + background: darken(@grayLighter, 1%); + outline: none; +} + +.umb-iconpicker-item a:active { + background: darken(@grayLighter, 4%); +} + +.umb-iconpicker-item i { + font-size: 30px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less new file mode 100644 index 0000000000..c603a3c7cd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-keyboard-shortcuts-overview.less @@ -0,0 +1,100 @@ + +/* ---------- OVERLAY ---------- */ + +.umb-keyboard-shortcuts-overview__overlay { + position: fixed; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: rgb(255, 255, 255); + z-index: 1000; + animation: fadeIn 0.2s; + box-sizing: border-box; + padding-left: 440px; +} + +.umb-keyboard-shortcuts-overview__overlay-close { + position: absolute; + top: 20px; + right: 20px; + font-size: 25px; + border-radius: 20px; + width: 35px; + height: 35px; + display: flex; + justify-content: center; + align-items: center; + background-color: transparent; + line-height: 1em; +} + +.umb-keyboard-shortcuts-overview__overlay-close:hover { + background-color: @blue; + color: #ffffff; + text-decoration: none; +} + +.umb-keyboard-shortcuts-overview__overlay-content { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} + +.umb-keyboard-shortcuts-overview__keyboard-shortcuts-group { + width: 50%; + margin-bottom: 10px; +} + +.umb-keyboard-shortcuts-overview__keyboard-shortcuts-group-name { + margin-bottom: 0; +} + +.umb-keyboard-shortcuts-overview__keyboard-shortcut { + display: flex; + justify-content: space-between; + align-items: center; + padding: 7px 0; + border-bottom: 1px solid @grayLight; +} + +.umb-keyboard-shortcuts-overview__keyboard-shortcut.-no-air { + padding: 0; +} + +.umb-keyboard-shortcuts-overview__keyboard-shortcut.-no-stroke { + border-bottom: none; +} + +.umb-keyboard-shortcuts-overview__description { + font-weight: bold; + font-size: 13px; + margin-right: 10px; +} + +/* ---------- KEYBOARD KEYS ---------- */ + +.umb-keyboard-keys { + list-style: none; + display: flex; + font-size: 12px; + align-items: center; +} + +.umb-keyboard-key-wrapper { + display: flex; + margin-right: 5px; + align-items: center; +} + +.umb-keyboard-key { + background: #ffffff; + border: 1px solid @grayLight; + color: @gray; + border-radius: 5px; + margin-right: 5px; + padding: 1px 7px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less new file mode 100644 index 0000000000..74d85461fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -0,0 +1,60 @@ +.umb-layout-selector { + display: inline-block; + position: relative; +} + +.umb-layout-selector__active-layout { + box-sizing: border-box; + border: 1px solid transparent; + cursor: pointer; + height: 30px; + width: 30px; + font-size: 20px; + display: flex; + justify-content: center; + align-items: center; +} + +.umb-layout-selector__active-layout:hover { + border-color: @grayLight; +} + +.umb-layout-selector__dropdown { + position: absolute; + padding: 5px; + background: #333; + z-index: 999; + display: flex; + background: #fff; + flex-wrap: wrap; + flex-direction: column; + transform: translate(-50%,0); + left: 50%; +} + +.umb-layout-selector__dropdown-item { + padding: 5px; + margin: 3px 5px; + display: flex; + align-content: center; + justify-content: center; + border: 1px solid transparent; + flex-direction: column; + cursor: pointer; +} + +.umb-layout-selector__dropdown-item:hover { + border: 1px solid @grayLight; +} + +.umb-layout-selector__dropdown-item.-active { + border: 1px solid @blue; +} + +.umb-layout-selector__dropdown-item-icon, +.umb-layout-selector__dropdown-item-icon[class^="icon-"], +.umb-layout-selector__dropdown-item-icon[class*=" icon-"] { + font-size: 20px; + color: @gray; + text-align: center; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less new file mode 100644 index 0000000000..8c0f310cf3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less @@ -0,0 +1,49 @@ +.umb-list-view-settings__box { + background: #f8f8f8; + border: 1px solid #CCCCCC; + display: flex; + animation: fadeIn 0.5s; + padding: 15px; + position: relative; +} + +.umb-list-view-settings__trigger { + margin-bottom: 20px; +} + +.umb-list-view-settings__box.-open { + border-bottom: transparent; +} + +.umb-list-view-settings__content { + display: flex; +} + +.umb-list-view-settings__list-view-icon { + font-size: 20px; + color: #d9d9d9; + margin-right: 10px; +} + +.umb-list-view-settings__name { + margin-right: 5px; + font-size: 14px; + font-weight: bold; + float: left; +} + +.umb-list-view-settings__create-new { + font-size: 12px; + color: @blue; +} + +.umb-list-view-settings__remove-new { + font-size: 12px; + color: @red; +} + +.umb-list-view-settings__settings { + border: 1px dashed #d9d9d9; + border-top: none; + padding: 20px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less new file mode 100644 index 0000000000..b2d03147a0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-load-indicator.less @@ -0,0 +1,55 @@ +.umb-load-indicator { + list-style: none; + margin: 0; + padding: 0; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 0; +} + +.umb-load-indicator__bubble { + height: 0; + position: absolute; + top: 50%; + left: 0; + width: 0; + margin: 0; + height: 6px; + width: 6px; + border: 2px solid @blue; + border-radius: 100%; + transform: transformZ(0); + animation: umbLoadIndicatorAnimation 1.4s infinite; +} + +.umb-load-indicator__bubble:nth-child(1n) { + left: -16px; + animation-delay: 0s; +} + +.umb-load-indicator__bubble:nth-child(2n) { + left: 0; + animation-delay: 0.15s; +} + +.umb-load-indicator__bubble:nth-child(3n) { + left: 16px; + animation-delay: 0.30s; +} + +@keyframes umbLoadIndicatorAnimation { + 0% { + transform: scale(0.5); + background: @blue; + } + 50% { + transform: scale(1); + background: white; + } + 100% { + transform: scale(0.5); + background: @blue; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less new file mode 100644 index 0000000000..d9c9b16f40 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less @@ -0,0 +1,49 @@ +.umb-locked-field { + font-size: 13px; + color: #ccc; + position: relative; + display: block; +} + +.umb-locked-field__wrapper { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.umb-locked-field__toggle { + margin-right: 3px; +} + +.umb-locked-field__toggle:hover, +.umb-locked-field__toggle:focus { + text-decoration: none; +} + +.umb-locked-field__lock-icon { + color: #ccc; + transition: color 0.25s; + //vertical-align: top; +} + +.umb-locked-field__lock-icon.-unlocked { + color: #515151; +} + +input.umb-locked-field__input { + background: rgba(255, 255, 255, 0); // if using transparent it will hide the text in safari + border-color: transparent !important; + font-size: 13px; + margin-bottom: 0; + color: #ccc; + transition: color 0.25s; + padding: 0; +} + +input.umb-locked-field__input:focus { + box-shadow: none !important; +} + +input.umb-locked-field__input.-unlocked { + color: #515151; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less new file mode 100644 index 0000000000..0f2305bd90 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -0,0 +1,143 @@ +.umb-media-grid { + display: flex; + flex-wrap: wrap; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + width: 100%; + margin-bottom: 30px; +} + +.umb-media-grid__item { + display: flex; + flex-direction: column; + flex-wrap: wrap; + + justify-content: center; + + align-content: center; + align-items: center; + align-self: stretch; + + margin: 10px; + position: relative; + overflow: hidden; + cursor: pointer; + + box-shadow: 0 1px 1px 0 rgba(0,0,0,.2); + + transition: box-shadow 100ms; +} + +.umb-media-grid__item.-file { + background-color: @grayLighter; +} + +.umb-media-grid__item.-selected { + box-shadow: 0 2px 8px 0 rgba(0,0,0,.35); +} + +.umb-media-grid__item:hover { + text-decoration: none; +} + +.umb-media-grid__item-image { + max-width: 100% !important; + height: auto; + position: relative; +} + +.umb-media-grid__item-image-placeholder { + width: 100%; + max-width: 100%; + height: auto; + position: relative; +} + +.umb-media-grid__image-background { + content: ""; + background: url("../img/checkered-background.png"); + background-repeat: repeat; + opacity: 0.5; + top: 0; + left: 0; + bottom: 0; + right: 0; + position: absolute; +} + +.umb-media-grid__item-overlay { + display: flex; + opacity: 0; + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + padding: 5px 10px; + box-sizing: border-box; + font-size: 11px; + overflow: hidden; + color: white; + white-space: nowrap; + background: @blue; + transition: opacity 150ms; +} + +.umb-media-grid__item.-file .umb-media-grid__item-overlay { + opacity: 1; + color: @gray; + background: @grayLighter; +} + +.umb-media-grid__item.-file:hover .umb-media-grid__item-overlay, +.umb-media-grid__item.-file.-selected .umb-media-grid__item-overlay { + color: @white; + background: @blue; +} + +.umb-media-grid__info { + margin-right: 5px; +} + +.umb-media-grid__item-overlay.-locked { + opacity: 1; +} + +.umb-media-grid__item:hover .umb-media-grid__item-overlay { + opacity: 1; +} + +.umb-media-grid__item-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.umb-media-grid__item-icon { + color: @gray; + position: absolute; + top: 45%; + left: 50%; + font-size: 40px !important; + transform: translate(-50%,-50%); +} + +.umb-media-grid__checkmark { + position: absolute; + z-index: 2; + top: 10px; + right: 10px; + width: 25px; + height: 25px; + border: 1px solid #ffffff; + background: @blue; + border-radius: 50px; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + color: #ffffff; + margin-left: 7px; + transition: background 100ms; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less new file mode 100644 index 0000000000..cbea6987e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-editor.less @@ -0,0 +1,3 @@ +.umb-property-editor.-not-clickable { + pointer-events: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less new file mode 100644 index 0000000000..b71d5fcc22 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-sub-views.less @@ -0,0 +1,40 @@ +.umb-sub-views { + + .umb-sub-views-action-bar { + margin-bottom: 40px; + } + + .umb-sub-views-action-bar .btn-link { + padding-left: 0; + padding-right: 0; + &:focus { + outline: none; + text-decoration: none; + } + } + + .umb-sub-views-nav { + float: right; + margin: 0; + .umb-sub-views-nav-item { + display: inline-block; + margin-left: 15px; + &.is-active { + .btn-link { + color: @blue !important; + } + } + } + } + + .umb-sub-views-tools { + float: left; + margin: 0; + .umb-sub-views-tool { + display: inline-block; + margin-right: 15px; + } + } + +} + diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less new file mode 100644 index 0000000000..874f9e9551 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -0,0 +1,227 @@ +// Table Styles +.umb-table { + display: flex; + flex-direction: column; + + border: 1px solid @grayLight; + + flex-wrap: nowrap; + justify-content: space-between; + + min-width: 640px; +} + +.umb-table a { + text-decoration: none; + cursor: pointer; + + &:focus { + outline: none; + text-decoration: none; + } +} + +input.umb-table__input { + margin: 0 auto; +} + + + + +// Table Head Styles +.umb-table-head { + text-transform: uppercase; + + background-color: @grayLighter; + + font-size: 11px; + font-weight: 600; +} + +.umb-table-head__link { + position: relative; + + cursor: default; + text-decoration: none; + + color: fade(@gray, 75%); + + + &:hover { + text-decoration: none; + color: fade(@gray, 75%); + } +} + +.umb-table-head__link .sortable { + &:hover { + text-decoration: none; + cursor: pointer; + color: @black; + } +} + +.umb-table-thead__icon { + position: absolute; + + padding-top: 1px; + padding-left: 3px; + + font-size: 13px; + + cursor: default; +} + +.umb-table-thead .sortable:hover { + cursor: pointer; + text-decoration: none; +} + + + + + +// Table Body Styles +.umb-table-body .umb-table-row { + color: fade(@gray, 75%); + border-top: 1px solid @grayLight; + cursor: pointer; + font-size: 13px; + + &:hover { + background-color: fade(@grayLighter, 90%); + } +} + +.umb-table-body__link { + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + +.umb-table-body__icon, +.umb-table-body__icon[class^="icon-"], +.umb-table-body__icon[class*=" icon-"] { + margin: 0 auto; + font-size: 22px; + line-height: 22px; + color: @blueDark; +} + +.umb-table-body__checkicon, +.umb-table-body__checkicon[class^="icon-"], +.umb-table-body__checkicon[class*=" icon-"] { + display: none; + font-size: 16px; + line-height: 22px; +} + +.umb-table-body .umb-table__name { + color: @black; + font-size: 14px; + font-weight: bold; +} + +// Styles of no items in the listview +.umb-table-body__empty { + font-size: 16px; + text-align: center; + + color: @gray; + + padding: 20px 0; + + height: 100%; +} + +// Show checkmark when checked, hide file icon +.-selected { + .umb-table-body__fileicon { + display: none; + } + + .umb-table-body__checkicon { + display: inline-block; + } +} + + +// Styling for when item is published or not +.-published { + +} + +.-content .-unpublished { + .umb-table__name > * { + opacity: .4; + } + + .umb-table-body__icon { + opacity: .4; + } +} + +.-selected.-unpublished { + .umb-table-body__icon { + opacity: 1; + } +} + + + + + + +// Table Row Styles +.umb-table-row { + display: flex; + flex-flow: row nowrap; + align-items: center; + + user-select: none; +} + +.umb-table-row.-selected, +.umb-table-row.-selected:hover { + background-color: fade(@blueDark, 4%); +} + + + + +// Table Cell Styles +.umb-table-cell { + display: flex; + flex-flow: row nowrap; + flex: 1 1 1%; //NOTE 1% is a Internet Explore hack, so that cells don't collapse + + position: relative; + + margin: auto 14px; + padding: 6px 2px; + + text-align: left; + overflow:hidden; +} + +.umb-table-cell > * { + overflow: hidden; + white-space: nowrap; //NOTE Disable/Enable this to keep textstring on one line + text-overflow: ellipsis; +} + +.umb-table-cell:first-of-type { + flex: 0 0 25px; + + margin: 0 0 0 15px; + padding: 15px 0; +} + + +// Increases the space for the name cell +.umb-table__name { + flex: 1 1 25%; + max-width: 25%; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less new file mode 100644 index 0000000000..df5306c86d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less @@ -0,0 +1,15 @@ +.umb-nav-tabs { + position: absolute; + z-index: 10; +} + +.umb-nav-tabs.-padding-left { + padding-left: 20px; +} + +.umb-tab-content { + padding-top: 20px; + position: relative; + top: 22px; + border-top: 1px solid @grayLight; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index dc59a4b6ab..f9dedf5902 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -16,16 +16,18 @@ label small, .guiDialogTiny { font-size: 11px } -label.control-label { - padding: 8px 10px 0 0 !important; +label.control-label, .control-label { + padding: 0 10px 0 0 !important; + font-weight: bold; } + .umb-status-label{ color: @gray !important; } -.controls-row label{padding: 0 10px 0 10px; vertical-align: center} +.controls-row label{padding: 0 10px 0 10px; vertical-align: middle;} @@ -67,6 +69,11 @@ label.control-label { left: 4px; color: @grayLight; } + +.form-search .icon-search { + pointer-events: none; +} + .form-search input { width: 90%; font-size: @fontSizeLarge; @@ -79,6 +86,28 @@ label.control-label { } +.form-search .search-input { + font-weight: bold; + border-color: @grayLight; + + &:hover, + &:focus, + &:focus:hover { + border-color: #ccc; + } + + &:-moz-placeholder { + font-weight: normal; + } + &:-ms-input-placeholder { + font-weight: normal; + } + &::-webkit-input-placeholder { + font-weight: normal; + } +} + + // GENERAL STYLES // -------------- @@ -87,6 +116,10 @@ form { margin: 0 0 @baseLineHeight; } +form.-no-margin-bottom { + margin-bottom: 0; +} + fieldset { padding: 0; margin: 0; @@ -131,7 +164,7 @@ textarea { // Identify controls by their labels label { - display: block; + display: inline-block; margin-bottom: 5px; } @@ -167,6 +200,13 @@ input[type="color"], vertical-align: middle; } +input.-full-width-input { + width: 100%; + box-sizing: border-box; + padding: 4px 6px; + height: 30px; +} + // Reset appearance properties for textual inputs and textarea // Declare width for legacy (can't be on input[type=*] selectors or it's too specific) input, @@ -302,6 +342,12 @@ textarea { min-height: @baseLineHeight; // clear the floating input if there is no label text padding-left: 20px; } + +.radio.no-indent, +.checkbox.no-indent { + padding-left: 0; +} + .radio input[type="radio"], .checkbox input[type="checkbox"] { float: left; @@ -423,44 +469,18 @@ input[type="checkbox"][readonly] { //SD: NOTE: Had to change these to use our 'form' prefixed colors since we cannot -// share colors with the notifications/alerts. Also had to change them so that +// share colors with the notifications/alerts. Also had to change them so that // we do not show any errors unless the containing element has the show-validation // class assigned. // FORM FIELD FEEDBACK STATES // -------------------------- -// Warning -.show-validation.ng-invalid .control-group.warning { - .formFieldState(@formWarningText, @formWarningText, @formWarningBackground); -} // Error -.show-validation.ng-invalid .control-group.error { +.show-validation.ng-invalid .control-group.error, +.show-validation.ng-invalid .umb-panel-header-content-wrapper { .formFieldState(@formErrorText, @formErrorText, @formErrorBackground); } -// Success -.show-validation.ng-invalid .control-group.success { - .formFieldState(@formSuccessText, @formSuccessText, @formSuccessBackground); -} -// Success -.show-validation.ng-invalid .control-group.info { - .formFieldState(@formInfoText, @formInfoText, @formInfoBackground); -} - -// HTML5 invalid states -// Shares styles with the .control-group.error above - -.show-validation input:focus:invalid, -.show-validation textarea:focus:invalid, -.show-validation select:focus:invalid { - color: @formErrorText; - border-color: #ee5f5b; - &:focus { - border-color: darken(#ee5f5b, 10%); - @shadow: 0 0 6px lighten(#ee5f5b, 20%); - .box-shadow(@shadow); - } -} //val-highlight directive styling .highlight-error { @@ -547,7 +567,6 @@ input[type="checkbox"][readonly] { margin-bottom: 0; // prevent bottom margin from screwing up alignment in stacked forms *margin-left: 0; vertical-align: top; - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); // Make input on top when focused so blue border and shadow always show &:focus { z-index: 2; @@ -584,31 +603,14 @@ input[type="checkbox"][readonly] { .btn { margin-right: -1px; } - .add-on:first-child, - .btn:first-child { - .border-radius(@inputBorderRadius 0 0 @inputBorderRadius); - } } .input-append { - input, - select, - .uneditable-input { - .border-radius(@inputBorderRadius 0 0 @inputBorderRadius); - + .btn-group .btn:last-child { - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - } - } .add-on, .btn, .btn-group { margin-left: -1px; } - .add-on:last-child, - .btn:last-child, - .btn-group:last-child > .dropdown-toggle { - .border-radius(0 @inputBorderRadius @inputBorderRadius 0); - } } // Remove all border-radius for inputs with both prepend and append @@ -735,6 +737,11 @@ input.search-query { margin-bottom: @baseLineHeight / 2; } +//modifier for control group +.control-group.-no-margin { + margin-bottom:0; +} + // Legend collapses margin, so next element is responsible for spacing legend + .control-group { margin-top: @baseLineHeight; @@ -807,4 +814,4 @@ legend + .control-group { margin-left: 0; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less new file mode 100644 index 0000000000..52ea313829 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less @@ -0,0 +1,44 @@ +.umb-validation-label { + position: relative; + padding: 1px 5px; + background: @red; + color: white; + font-size: 10px; + line-height: 1.5em; +} + +.umb-validation-label:after { + bottom: 100%; + left: 10px; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(255, 255, 255, 0); + border-bottom-color: @red; + border-width: 4px; + margin-left: -4px; +} + +.umb-validation-label.-arrow-left { + margin-left: 10px; +} + +.umb-validation-label.-arrow-left:after { + right: 100%; + top: 50%; + left: auto; + bottom: auto; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(255, 255, 255, 0); + border-right-color: @red; + border-width: 4px; + margin-top: -4px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 04da8fb0af..fb809af015 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -18,14 +18,24 @@ .controls-row img { max-width: none; } + +.thumbnail { + border-radius: 0px; +} + .thumbnail img { - max-width: 100% !important + max-width: 100% !important; + width: 100%; } #mapCanvas img { max-width: none !important; } +.btn-group .dropdown-backdrop { + display: none; +} + /* loading animation for iframes and content pages */ iframe, .content-column-body { background: center center url(../img/loader.gif) no-repeat; @@ -48,14 +58,18 @@ iframe, .content-column-body { transform: translate(-300px, 0) scale(4); font-size: 23px; direction: ltr; - cursor: pointer; + cursor: pointer; } /*tree legacy icon*/ -.legacy-custom-file{ - width: 16px; height: 16px; margin-right: 11px; display: inline-block; +.legacy-custom-file { + width: 16px; + height: 16px; + min-width: 20px; /* this ensure the icon takes up same space as font-icon (20px) */ + display: inline-block; background-position: center center; + background-repeat: no-repeat; } /* @@ -67,4 +81,95 @@ iframe, .content-column-body { } .icon-chevron-down:before { content: "\e0c9"; -} \ No newline at end of file +} + + +/* Styling for validation in Public Access */ + +.pa-umb-overlay { + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.pa-umb-overlay + .pa-umb-overlay { + padding-top: 30px; + border-top: 1px solid @grayLight; +} + +.pa-select-type { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + justify-content: center; + align-items: flex-start; + + margin-top: 15px; +} + +.pa-select-type label { + padding: 0 20px; +} + +.pa-access-header { + font-weight: bold; + margin: 0 0 3px 0; + padding-bottom: 0; +} + +.pa-access-description { + color: #b3b3b3; + margin: 0; +} + +.pa-validation-message { + padding: 6px 12px !important; + margin: 5px 0 0 0 !important; + display: inline-block; +} + +.pa-select-pages label { + margin: 0; + font-size: 15px; +} + +.pa-select-pages label + .controls-row { + padding-top: 0; +} + +.pa-select-pages .umb-detail { + font-size: 13px; + margin: 2px 0 5px; +} + +.pa-choose-page a { + color: @blue; + font-size: 15px; +} + +.pa-choose-page a:hover, .pa-choose-page a:active, .pa-choose-page a:focus { + color: @blueDark; + text-decoration: none; +} + +.pa-choose-page a:before { + content:"+"; + margin-right: 3px; + font-weight: bold; +} + +.pa-choose-page .treePickerTitle { + font-weight: bold; + font-size: 13px; + font-style: italic; + background: whitesmoke; + padding: 3px 5px; + color: grey; + + border-bottom: none; +} + + +.pa-form + .pa-form { + margin-top: 10px; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 574fde27a9..aa8bbccfef 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -38,7 +38,7 @@ body { line-height: @baseLineHeight; color: @textColor; - vertical-align: center; + vertical-align: middle; text-align: center; } diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index afa1864e0b..88762edf0c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -13,9 +13,9 @@ box-shadow: 0 5px 10px rgba(0, 0, 0, 0); } -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus, -.dropdown-submenu:hover > a, +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-submenu:hover > a, .dropdown-submenu:focus > a { color: @black; background: @grayLighter; @@ -32,7 +32,11 @@ } .umb-sub-header { - padding: 0 0 20px 0 + padding: 0 0 20px 0; +} + +.umb-sub-header .header-content-right { + float: right; } /* listview search */ @@ -120,7 +124,7 @@ } .umb-listview .selected i.icon, .umb-listview tbody tr:hover i.icon{display: none} -.umb-listview .selected input[type="checkbox"], +.umb-listview .selected input[type="checkbox"], .umb-listview tr:hover input[type="checkbox"]{display: inline-block !important;} .umb-listview .inactive{color: @grayLight;} @@ -130,7 +134,7 @@ font-size: 11px; font-weight: 600; text-transform: uppercase; - background-color: @grayLighter; + background-color: @white; } .umb-listview table tfoot { @@ -177,6 +181,10 @@ color: #b0b0b0 } +.umb-listview .pagination { + text-align: center; +} + .umb-listview .pagination ul { -webkit-border-radius: 0px; -moz-border-radius: 0px; @@ -191,7 +199,7 @@ border:none; padding: 8px 4px 2px 4px; background: none; - font-size: 11px; + font-size: 12px; color: #b0b0b0 } @@ -235,6 +243,87 @@ background-color: @grayLighter } -.table-striped tbody i:hover { +/* don't hide all icons, e.g. for a sortable handle */ +.umb-listview .table-striped tbody i:not(.handle):hover { display: none !important } + +/* ---------- LAYOUTS ---------- */ + +.list-view-layouts { + +} + +.list-view-layout { + display: flex; + align-items: center; + padding: 10px 15px; + background: @grayLighter; + margin-bottom: 1px; +} + +.list-view-layout__sort-handle { + font-size: 14px; + color: @grayLight; + margin-right: 15px; +} + +.list-view-layout__name { + flex: 5; + font-weight: bold; + margin-right: 15px; + display: flex; + align-content: center; + flex-wrap: wrap; + line-height: 1.2em; +} + +.list-view-layout__name-text { + margin-right: 3px; +} + +.list-view-layout__system { + font-size: 10px; + font-weight: normal; +} + +.list-view-layout__path { + flex: 10; + margin-right: 15px; +} + +.list-view-layout__icon { + font-size: 18px; + margin-right: 10px; + vertical-align: middle; + border: 1px solid @grayLight; + background: #ffffff; + padding: 6px 8px; + display: block; +} + +.list-view-layout__icon:hover, +.list-view-layout__icon:focus, +.list-view-layout__icon:active { + text-decoration: none; +} + +.list-view-layout__remove { + position: relative; + cursor: pointer; +} + +.list-view-add-layout { + margin-top: 10px; + color: @blue; + border: 1px dashed #d9d9d9; + display: flex; + align-items: center; + justify-content: center; + padding: 5px 0; + box-sizing: border-box; +} + +.list-view-add-layout:hover { + text-decoration: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/login.less b/src/Umbraco.Web.UI.Client/src/less/login.less deleted file mode 100644 index c94414df10..0000000000 --- a/src/Umbraco.Web.UI.Client/src/less/login.less +++ /dev/null @@ -1,82 +0,0 @@ -// Login -// ------------------------- - -.login-overlay { - width: 100%; - height: 100%; - background: @blackLight url(../img/application/logo.png) no-repeat 25px 30px fixed !important; - background-size: 30px 30px !important; - color: @white; - position: absolute; - z-index: 2000; - top: 0px; - left: 0px; - margin: 0 !Important; - padding: 0; - border-radius: 0; -} - -.login-overlay .umb-modalcolumn { - background: none; - border: none; -} - -.login-overlay .form { - position:fixed; - display: block; - top: 100px; - left: 165px; - width: 370px; - text-align: right; -} - -.login-overlay h1 { - display: block; - text-align: right; - color: @white; - font-size: 18px; - font-weight: normal; -} - -.login-overlay .alert.alert-error { - display: inline-block; - padding-right: 6px; - padding-left: 6px; - margin-top: 10px; - text-align: center; -} - -@media (max-width: 565px) { - // Remove padding on login-form on smaller devices - .login-overlay .form { - left: inherit; - right:25px; - } -} - -#hrOr { - height: 30px; - text-align: center; - position: relative; - padding-top: 20px; -} - -#hrOr hr { - margin: 0px; - border: none; - background-color: @gray; - height: 1px; -} - -#hrOr div { - background-color: black; - position: relative; - top: -16px; - border: 1px solid @gray; - padding: 4px; - border-radius: 50%; - width: 20px; - height: 20px; - margin: auto; - color: @grayLight; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index fcdc683182..00cdd4f9b1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -34,10 +34,14 @@ h5{ margin-top: 15px; } +h5.-border-bottom { + border-bottom: 1px solid #ECECEC; + padding-bottom: 5px; +} - - - +h5.-black { + color: @black; +} /* MISC FORM ELEMENTS */ .umb-form-actions { @@ -106,6 +110,11 @@ h5{ margin-bottom: 15px !important; } +.umb-control-group.-no-border { + border: none; +} + + /*COMPACT MODE */ .compact .umb-pane{margin: 0px 0px 15px 0px;} .compact .umb-control-group { @@ -130,10 +139,13 @@ h5{ } /* LABELS*/ -.umb-control-group label.control-label { - text-align: left +.umb-control-group label.control-label, .umb-control-group .control-label { + text-align: left; } -.umb-control-group label .help-block, +.umb-control-group label.control-label > div > label { + padding-left: 0; +} +.umb-control-group label .help-block, .umb-control-group label small { font-size: 11px; color: #a0a0a0; @@ -141,21 +153,49 @@ h5{ padding-top: 5px; } .umb-nolabel .controls { - margin-left: 0px; + margin-left: 0; } .controls-row { padding-top: 5px; margin-left: 240px; } -.controls-row label { - display: inline-block + +.umb-user-panel .controls-row { + margin-left: 0; } -.hidelabel > div > .controls-row, .hidelabel > .controls-row { - padding: 0px; +.controls-row label { + display: inline-block; +} + +.controls-row > div > label { + padding-left: 0; +} + +.block-form .controls-row { + margin-left: 0; + padding-top: 0; +} + +.hidelabel > div > .controls-row, .hidelabel > .controls-row, .hidelabel > div > .controls { + padding: 0; border: none; - margin: 0px !important; + margin: 0 !important; +} + +.controls-row > .vertical-align-items { + display: flex; + align-items: center; +} + +.controls-row > .vertical-align-items > input.umb-editor-tiny { + margin-left: 5px; + margin-right: 5px; +} + +.controls-row > .vertical-align-items > input.umb-editor-tiny:first-child { + margin-left: 0; } .thumbnails .selected { @@ -187,31 +227,10 @@ h5{ left: 2px } -.umb-dashboard-control a{text-decoration: underline;} - -.umb-dashboard-control +.umb-dashboard-control iframe{ position: absolute; display: block; width: 99%; height: 99%; overflow: auto !important;} -/* NOTIFICATIONS */ -.umb-notification ul { - list-style: none; - margin: 0; - padding-left: 100px; - padding-right: 20px; - position: relative; -} -.umb-notification li { - padding: 5px 30px 5px 20px; - text-shadow: none; - font-size: 12px; - margin: auto; - margin-top: 5px; - border: none; - border-radius: 5; - position: relative; -} - /* TABLE */ .umb-table { table-layout: fixed; @@ -234,16 +253,16 @@ table thead a { .umb-table tbody.ui-sortable tr.ui-sortable-helper { background-color: @sortableHelperBg; - border: none; + border: none; } -.umb-table tbody.ui-sortable tr.ui-sortable-helper td +.umb-table tbody.ui-sortable tr.ui-sortable-helper td { border:none; } .umb-table tbody.ui-sortable tr.ui-sortable-placeholder { background-color: @sortablePlaceholderBg; - border:none; + border:none; } .umb-table tbody.ui-sortable tr.ui-sortable-placeholder td @@ -363,7 +382,7 @@ table thead a { background: @inputBorder; height: 1px; margin: 20px 0 0 0; - width: 82%; + width: 82%; float: left; } @@ -432,7 +451,7 @@ table thead a { color:@green; } -// Loading Animation +// Loading Animation // ------------------------ .umb-loader{ @@ -518,11 +537,79 @@ height:1px; } .umb-loader-wrapper { - + position: absolute; right: 0; left: 0; margin: 10px 0; - overflow: hidden; + overflow: hidden; -} \ No newline at end of file +} + +.umb-loader-wrapper.-bottom { + bottom: 0; +} + +// Helpers + +.strong { + font-weight: bold; +} + +.inline { + display: inline; +} + + +// Input label styles +// @Simon: not sure where to put this part yet +// --- TODO Needs to be divided into the right .less directories + + +// Titles for input fields +.input-label--title { + font-weight: bold; + color: @black; + + margin-bottom: 3px; +} + + +// Used for input checkmark fields +.input-label--small { + display: inline; + + font-size: 12px; + font-weight: bold; + color: fade(@black, 70); + + &:hover { + color: @black; + } +} + +input[type=checkbox]:checked + .input-label--small { + color: @blue; +} + + +// Use this for headers in the panels +.panel-dialog--header { + border-bottom: 1px solid @gray; + + margin: 10px 0; + padding-bottom: 10px; + + font-size: @fontSizeLarge; + font-weight: bold; + line-height: 20px; +} + + +// Datepicker styles +.bootstrap-datetimepicker-widget, +.bootstrap-datetimepicker-widget td, +.bootstrap-datetimepicker-widget th, +.bootstrap-datetimepicker-widget td span { + border-radius: 0 !important; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index e1f9170424..8e986b5b00 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -161,7 +161,7 @@ // Mixin for form field states -//SD: I've had to modify this slightly to work nicely with angular validation , note the +//SD: I've had to modify this slightly to work nicely with angular validation , note the // additional targetting of the ng-invalid class. .formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) { // Set the text color @@ -182,12 +182,6 @@ select.ng-invalid, textarea.ng-invalid { border-color: @borderColor; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@borderColor, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@borderColor, 20%); - .box-shadow(@shadow); - } } // Give a small background color for input-prepend/-append .input-prepend .add-on, @@ -196,6 +190,18 @@ background-color: @backgroundColor; border-color: @textColor; } + //SD: We could do this but need to get the colors right + /*input.ng-invalid { + &:-moz-placeholder { + color: lighten(@textColor, 50%) !important; + } + &:-ms-input-placeholder { + color: lighten(@textColor, 50%) !important; + } + &::-webkit-input-placeholder { + color: lighten(@textColor, 50%) !important; + } + }*/ } // CSS3 PROPERTIES @@ -516,10 +522,14 @@ *background-color: @endColor; /* Darken IE7 buttons by default so they stand out more given they won't have borders */ .reset-filter(); + // button states + &:hover, &:focus, &:active { + background-color: darken(@startColor, 2%); + } + // in these cases the gradient won't cover the background, so we override &:hover, &:focus, &:active, &.active, &.disabled, &[disabled] { color: @textColor; - background-color: @endColor; *background-color: darken(@endColor, 5%); } diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index c79f4b6420..c8291f0c67 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -131,20 +131,18 @@ } // Actual tabs (as links) .nav-tabs > li > a { - -webkit-border-radius: @tabsBorderRadius; - border-radius: @tabsBorderRadius; color: @gray; padding-top: 5px; padding-bottom: 4px; line-height: @baseLineHeight; border: 1px solid transparent; - .border-radius(4px 4px 0 0); + &:hover { color: @black; } &:hover, &:focus { - border-color: @grayLighter @grayLighter #ddd; + border-color: @grayLighter @grayLighter #ddd; } } // Active state, and it's :hover/:focus to override normal :hover/:focus @@ -249,6 +247,14 @@ // DROPDOWNS // --------- +.dropdown-menu { + border-radius: 0; +} + +.dropdown-menu > li > a { + padding: 8px 20px; +} + .nav-tabs .dropdown-menu { .border-radius(0 0 6px 6px); // remove the top rounded corners here since there is a hard edge above the menu } diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/document-type-editor.less b/src/Umbraco.Web.UI.Client/src/less/pages/document-type-editor.less new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less new file mode 100644 index 0000000000..892977180e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -0,0 +1,141 @@ +// Login +// ------------------------- + +.login-overlay { + width: 100%; + height: 100%; + background: @blackLight url(../img/application/logo.png) no-repeat 25px 30px fixed !important; + background-size: 30px 30px !important; + color: @white; + position: absolute; + z-index: 10000; + top: 0; + left: 0; + margin: 0 !important; + padding: 0; + border: none; + border-radius: 0; + overflow-y: auto; +} + + +@media only screen and (-webkit-min-device-pixel-ratio: 1.5), + only screen and (min-resolution: 144dpi) +{ + .login-overlay { + background-image: url(../img/application/logo@2x.png) !important; + } +} + + +@media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi) +{ + .login-overlay { + background-image: url(../img/application/logo@2x.png) !important; + } +} + + +@media only screen and (-webkit-min-device-pixel-ratio: 3), + only screen and (min-resolution: 3dppx), + only screen and (min-resolution: 350dpi) +{ + .login-overlay { + background-image: url(../img/application/logo@3x.png) !important; + } +} + +.login-overlay .umb-modalcolumn { + background: none; + border: none; +} + +.login-overlay .form { + position:relative; + display: block; + top: 100px; + left: 165px; + width: 370px; + text-align: right; +} + +.login-overlay h1 { + display: block; + text-align: right; + color: @white; + font-size: 18px; + font-weight: normal; +} + +.login-overlay .alert { + display: inline-block; + padding-right: 6px; + padding-left: 6px; + margin-top: 10px; + text-align: center; +} + +.login-overlay .switch-view { + margin-top: 10px; +} + +@media (max-width: 767px) and (max-height: 420px) and (orientation: landscape) { + // Move form closer to top on narrow screen sizes + .login-overlay .form { + top: 50px; + } +} + +@media (max-width: 565px) { + // Remove padding on login-form on smaller devices + .login-overlay .form { + top: 60px; + right: 25px; + left: inherit; + padding-left: 25px; + padding-right:25px; + width: auto; + } +} + +@media (max-width: 339px) { + .login-overlay .form { + input[type="text"], input[type="password"] { + width: 250px; + } + } +} + +#hrOr { + height: 30px; + text-align: center; + position: relative; + padding-top: 20px; +} + +#hrOr hr { + margin: 0; + border: none; + background-color: @gray; + height: 1px; +} + +#hrOr div { + background-color: black; + position: relative; + top: -16px; + border: 1px solid @gray; + padding: 4px; + border-radius: 50%; + width: 20px; + height: 20px; + margin: auto; + color: @grayLight; +} + +.login-overlay .text-error, +.login-overlay .text-info +{ + font-weight:bold; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index f86ba46f59..a1776bf12d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -42,6 +42,23 @@ bottom: 90px; } +.umb-mediapicker-upload { + display: -ms-flexbox; + display: -webkit-box; + display: -webkit-flex; + display: flex; + + .form-search { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + } + + .upload-button { + margin-left: 16px; + } +} + .umb-panel.editor-breadcrumb .umb-panel-body, .umb-panel.editor-breadcrumb .umb-bottom-bar { bottom: 31px !important; } @@ -75,13 +92,6 @@ color: @red; } -.umb-panel-header .umb-nav-tabs { - bottom: -1px; - position: absolute; - padding: 0px 0px 0px 20px; -} - - .umb-panel-header p { margin: 0px 20px; } @@ -106,25 +116,19 @@ color: @red; } +.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder, +.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder, .umb-headline-editor-wrapper input.ng-invalid::-webkit-input-placeholder { color: @red; line-height: 22px; } -.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder { - color: @red; - line-height: 22px; -} - -.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder { - color: @red; - line-height: 22px; -} - +/* .umb-panel-header i { font-size: 13px; vertical-align: middle; } +*/ .umb-panel-header-meta { height: 50px; @@ -153,6 +157,7 @@ border-radius: @tabsBorderRadius; box-shadow: none; padding: 0; + z-index: 6020; } .umb-btn-toolbar .dropdown-menu small { @@ -274,9 +279,6 @@ display: none; } - - - // Utility classes //SD: Had to add these because if we want to use the bootstrap text colors @@ -348,3 +350,146 @@ .external-logins button { margin:5px; } + +/* --------- UMB PANEL HEADER ---------- */ + +.umb-panel-header-content-wrapper { + display: flex; + flex-direction: column; + height: 100px; + padding: 0 20px; +} + +.umb-panel-header-content { + display: flex; + align-items: center; + flex: 1; +} + +.umb-panel-header-content.-top-position { + position: relative; + top: -12px; +} + +.umb-panel-header-left-side { + display: flex; + flex: 1; + flex-direction: row; +} + +.umb-panel-header-icon { + cursor: pointer; + margin-right: 5px; + height: 55px; + display: flex; + justify-content: center; + align-items: center; + background: #ffffff; + border: 1px solid #ECECEC; + animation: fadeIn 0.5s; + flex: 0 0 55px; +} + +.umb-panel-header-title-wrapper { + position: relative; + width: 80%; +} + +.umb-panel-header-alias { + position: absolute; + top: 5px; + right: 10px; +} + +.umb-panel-header-alias .umb-locked-field { + display: flex; + align-items: center; +} + +.umb-panel-header-alias .umb-locked-field, +.umb-panel-header-alias .umb-locked-field .umb-locked-field__wrapper { + margin-bottom: 0; +} + +.umb-panel-header-alias .umb-validation-label:after { + visibility: hidden; +} + +.umb-panel-header-alias .umb-locked-field:after { + display: none; +} + +.umb-panel-header-icon.-placeholder { + border: 1px dashed @grayLight; +} + +.umb-panel-header-icon .icon { + font-size: 35px; + color: @grayLight; +} + +.umb-panel-header-icon-text { + color: @blue; + font-weight: bold; + font-size: 10px; +} + +.umb-panel-header .umb-nav-tabs { + bottom: -1px; +} + +input.umb-panel-header-name-input { + border-color: #ECECEC; + font-size: 15px; + color: #000000; + margin-bottom: 0; + font-weight: bold; + box-sizing: border-box; + height: 30px; + line-height: 30px; + width: 100%; + &:hover { + background: #ffffff; + border: 1px solid #cccccc; + } +} + +input.umb-panel-header-name-input.name-is-empty { + border: 1px dashed @grayLight; + background: #ffffff; +} + +.umb-panel-header-name { + font-size: 16px; + font-weight: bold; +} + + +input.umb-panel-header-description { + background: transparent; + border-color: transparent; + margin-bottom: 0; + font-size: 12px; + box-sizing: border-box; + height: 25px; + line-height: 25px; + width: 100%; + &:hover { + background: #ffffff; + border-color: #cccccc; + } +} + +.umb-editor-drawer-content { + display: flex; + align-items: center; + //justify-content: space-between; +} + +.umb-editor-drawer-content__right-side { + margin-left: auto; +} + +.umb-editor-drawer-content__left-side { + margin-right: auto; +} 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 be6b1d7b06..85a1d9e36c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -40,8 +40,16 @@ padding: 10px; } -.umb-contentpicker small a { +.umb-contentpicker small { + + &:not(:last-child) { + padding-right: 3px; + border-right: 1px solid @grayMed; + } + + a { color: @gray; + } } /* CODEMIRROR DATATYPE */ @@ -62,7 +70,7 @@ div.umb-codeeditor .umb-btn-toolbar { // // RTE // -------------------------------------------------- -.mce-tinymce{border: 1px solid @grayLight !important; } +.mce-tinymce{border: 1px solid @grayLight !important; border-radius: 0px !important;} .mce-panel{background: @grayLighter !important; border-color: @grayLight !important;} .mce-btn-group, .mce-btn{border: none !important; background: none !important;} .mce-ico{font-size: 12px !important; color: @blackLight !important;} @@ -73,6 +81,9 @@ div.umb-codeeditor .umb-btn-toolbar { font-size:16px !important; } +/* pre-value editor */ +.rte-editor-preval .control-group .controls > div > label .mce-ico { line-height: 20px; } + // // Color picker @@ -114,18 +125,25 @@ ul.color-picker li a { // // Media picker // -------------------------------------------------- -.umb-mediapicker .add-link{ +.umb-mediapicker .add-link { display: inline-block; height: 120px; width: 120px; text-align: center; color: @grayLight; - border: 2px @grayLight dashed !important; + border: 2px @grayLight dashed; line-height: 120px; text-decoration: none; + + transition: all 150ms ease-in-out; + + &:hover { + color: @blue; + border-color: @blue; + } } -.umb-mediapicker .picked-image{ +.umb-mediapicker .picked-image { position: absolute; bottom: 10px; right: 10px; @@ -166,27 +184,42 @@ ul.color-picker li a { list-style-type: none; margin: 0; padding: 0; - display: block; + display: flex; + flex-direction: row; + flex-wrap: wrap; } -.umb-sortable-thumbnails li{ - display: inline-block; - border: 1px solid @grayLighter; - padding: 2px; - background: white; - margin: 5px; +.umb-sortable-thumbnails li { position: relative; - text-align: center; - vertical-align: top; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + flex-wrap: wrap; + padding: 2px; + margin: 5px; + background: white; + border: 1px solid #f8f8f8; + + max-width: 100%; } -.umb-sortable-thumbnails li:hover a{ - display: block; + +.umb-mediapicker .umb-sortable-thumbnails li { + flex-direction: column; + margin: 0; + padding: 5px; } -.umb-sortable-thumbnails li img -{ + +.umb-sortable-thumbnails li:hover a { + display: flex; + justify-content: center; + align-items: center; +} + +.umb-sortable-thumbnails li img { max-width:100%; max-height:100%; margin:auto; @@ -199,14 +232,52 @@ ul.color-picker li a { max-height: none !important; } -.umb-sortable-thumbnails .icon-holder .icon{ - font-size: 60px; - line-height: 70px; +.umb-sortable-thumbnails .icon-holder { + text-align: center; } -.umb-sortable-thumbnails .icon-holder * -{ - color: @grayLight; - display: block + +.umb-sortable-thumbnails .icon-holder .icon{ + font-size: 40px; + line-height: 50px; + color: @gray; + display: block; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__actions { + position: absolute; + bottom: 10px; + right: 10px; + text-decoration: none; + display: flex; + flex-direction: row; + opacity: 0; + visibility: hidden; +} + +.umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__actions { + opacity: 1; + visibility: visible; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__action { + font-size: 16px; + background: white; + height: 25px; + width: 25px; + border-radius: 15px; + color: @grayDarker; + display: flex; + justify-content: center; + align-items: center; + margin-left: 5px; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__action.-red { + color: red; +} + +.umb-sortable-thumbnails .umb-sortable-thumbnails__action:hover { + text-decoration: none; } @@ -216,33 +287,41 @@ ul.color-picker li a { .umb-cropper{ position: relative; - padding: 10px; } .umb-cropper img, .umb-cropper-gravity img{ - position: absolute; + position: relative; + max-width: 100%; + height: auto; top: 0; left: 0; } + .umb-cropper img { + max-width: none; + } + .umb-cropper .overlay, .umb-cropper-gravity .overlay { top: 0; left: 0; cursor: move; z-index: 6001; + position: absolute; } .umb-cropper .viewport{ overflow: hidden; position: relative; margin: auto; + max-width: 100%; + height: auto; } .umb-cropper-gravity .viewport{ overflow: hidden; position: relative; - width: 400px; - height: 300px + width: 100%; + height: 100%; } @@ -272,30 +351,49 @@ ul.color-picker li a { opacity: 0.8; } -.umb-cropper-gravity .overlay i{ +.umb-cropper-gravity .overlay i { font-size: 26px; line-height: 26px; opacity: 0.8 !important; } -.umb-cropper .crop-container{ +.umb-cropper .crop-container { text-align: center; } -.umb-cropper .crop-slider{ - vertical-align: middle; - padding: 10px 50px 10px 50px; +.umb-cropper .crop-slider { + padding: 10px; border-top: 1px solid @grayLighter; 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{color: @gray;} -.umb-cropper .crop-slider input{ - margin-top: 7px; - width: 320px; +.umb-cropper .crop-slider i { + color: @gray; + flex: 0 0 25px; + padding: 0 5px; + box-sizing: border-box; +} + +.umb-cropper .crop-slider i:first-of-type { + text-align: right; +} + +.umb-cropper .crop-slider input { + flex: 0 1 auto; } .umb-cropper-gravity .viewport, .umb-cropper-gravity, .umb-cropper-imageholder { display: inline-block; + max-width: 100%; } .umb-cropper-imageholder { @@ -309,55 +407,98 @@ ul.color-picker li a { } .gravity-container .viewport { - width: 494px; + max-width: 600px; + } + + .gravity-container .viewport:hover { + cursor: pointer; + } + + .imagecropper { + display: flex; + align-items: flex-start; + flex-direction: row; + + @media (max-width: 768px) { + flex-direction: column; + float: left; + max-width: 100%; + } + } + + .imagecropper .umb-cropper__container { + position: relative; + margin-bottom: 10px; + max-width: 100%; + border: 1px solid #f8f8f8; + + @media (min-width: 769px) { + width: 600px; + } + } + + .umb-close-cropper { + position: absolute; + top: 3px; + right: 3px; + cursor: pointer; + } + + .umb-close-cropper:hover { + opacity: .9; + background: @grayLighter; } .imagecropper .umb-sortable-thumbnails { - border-left: 2px solid #f8f8f8; - margin-left: 4px; - padding-left: 2px; - float: left; - width: 100px; + display: flex; + flex-direction: row; + flex-wrap: wrap; } .imagecropper .umb-sortable-thumbnails li { - display: block; - } + display: flex; + flex-direction: column; + justify-content: space-between; - .imagecropper .umb-sortable-thumbnails li.current a, .imagecropper .umb-sortable-thumbnails li.current a:hover { - background: #eeeeee; - text-decoration: none; - } - - .imagecropper .umb-sortable-thumbnails li:first-of-type { + padding: 8px; margin-top: 0; - padding-top: 0; } - .imagecropper .umb-sortable-thumbnails li .crop-name, .imagecropper .umb-sortable-thumbnails li .crop-size { + .imagecropper .umb-sortable-thumbnails li.current { + border-color: @grayLight; + background: @grayLighter; + color: @black; + cursor: pointer; + } + + .imagecropper .umb-sortable-thumbnails li:hover, + .imagecropper .umb-sortable-thumbnails li.current:hover { + border-color: @grayLight; + background: @grayLighter; + color: @black; + + cursor: pointer; + + opacity: .95; + } + + .imagecropper .umb-sortable-thumbnails li .crop-name, + .imagecropper .umb-sortable-thumbnails li .crop-size { display: block; text-align: left; - font-size: 11px; + font-size: 13px; + line-height: 1; + } + + .imagecropper .umb-sortable-thumbnails li .crop-name { + font-weight: bold; + margin: 10px 0 5px; } .imagecropper .umb-sortable-thumbnails li .crop-size { font-size: 10px; font-style: italic; - color: #666; - } - - .imagecropper .umb-sortable-thumbnails li a { - display: block; - padding: 5px; - } - - .imagecropper .umb-sortable-thumbnails li a:hover { - background: #f8f8f8; - text-decoration: none; - } - - .imagecropper .umb-sortable-thumbnails li a:hover .crop-text { - text-decoration: none; + margin: 0 0 5px; } .btn-crop-delete { @@ -365,20 +506,6 @@ ul.color-picker li a { text-align: left; } - .imagecropper .umb-sortable-thumbnails.many { - width: 210px; - } - - .imagecropper .umb-sortable-thumbnails.many li { - width: 85px; - float: left; - } - - .imagecropper .umb-sortable-thumbnails.many li:nth-child(2) { - margin-top: 0; - padding-top: 0; - } - // // folder-browser // -------------------------------------------------- @@ -552,18 +679,21 @@ ul.color-picker li a { background: #efefef; float: left; margin-right: 30px; - margin-bottom: 30px + margin-bottom: 30px; } + .umb-fileupload ul { list-style: none; vertical-align: middle; - margin-bottom: 0px + margin-bottom: 0px; } + .umb-fileupload label { vertical-align: middle; padding-left: 7px; - font-weight: normal + font-weight: normal; } + .umb-fileupload .preview-file { color: #666; height: 45px; @@ -571,10 +701,12 @@ ul.color-picker li a { text-align: center; text-transform: uppercase; font-size: 10px; - padding-top: 27px + padding-top: 27px; } + .umb-fileupload input { - font-size: 12px + font-size: 12px; + line-height: 1; } // @@ -592,7 +724,45 @@ ul.color-picker li a { } // -// tags +// Related links +// -------------------------------------------------- +.umb-relatedlinks table > tr > td { word-wrap:break-word; word-break: break-all; border-bottom: 1px solid transparent; } +.umb-relatedlinks .handle { cursor:move; } +.umb-relatedlinks table > tbody > tr.unsortable .handle { cursor:default; } + +.umb-relatedlinks table td.col-sort { width: 20px; } +.umb-relatedlinks table td.col-caption { min-width: 200px; } +.umb-relatedlinks table td.col-link { min-width: 200px; } +.umb-relatedlinks table td.col-actions { min-width: 120px; } + +.umb-relatedlinks table td.col-caption .control-wrapper, +.umb-relatedlinks table td.col-link .control-wrapper { display: flex; } + +.umb-relatedlinks table td.col-caption .control-wrapper input[type="text"], +.umb-relatedlinks table td.col-link .control-wrapper input[type="text"] { width: auto; flex: 1; } + +/* sortable placeholder */ +.umb-relatedlinks .sortable-placeholder { + background-color: @tableBackgroundAccent; + display: table-row; +} +.umb-relatedlinks .sortable-placeholder > td { + display: table-cell; + padding: 8px; +} +.umb-relatedlinks .ui-sortable-helper { + display: table-row; + background-color: @white; + opacity: 0.7; +} +.umb-relatedlinks .ui-sortable-helper > td { + display: table-cell; + border-bottom: 1px solid @tableBorder; +} + + +// +// Tags // -------------------------------------------------- .umb-tags{border: @grayLighter solid 1px; padding: 10px; font-size: 13px; text-shadow: none;} .umb-tags .tag{cursor: pointer; margin: 7px; padding: 7px; background: @blue} @@ -612,7 +782,14 @@ ul.color-picker li a { // Code mirror - even though this isn't a proprety editor right now, it could be so I'm putting the styles here // -------------------------------------------------- -.CodeMirror, .CodeMirror-scroll { +.CodeMirror, .CodeMirror-scroll { height: 100%; min-height:200px; -} \ No newline at end of file +} + +// +// Nested boolean (e.g. list view bulk action permissions) +// ---------------------=====----------------------------- +.umb-nested-boolean label {margin-bottom: 8px; float: left; width: 320px;} +.umb-nested-boolean label span {float: left; width: 80%;} +.umb-nested-boolean label input[type='checkbox'] {margin-right: 10px; float: left;} diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 9ba3855b60..c89be83a10 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -19,8 +19,11 @@ ul.sections li { } ul.sections li [class^="icon-"]:before, -ul.sections li [class*=" icon-"]:before{ +ul.sections li [class*=" icon-"]:before, +ul.sections li img.icon-section { font-size: 30px; + line-height: 20px; /* set line-height to ensure all icons use same line-height */ + display: inline-block; margin: 1px 0 0 0; opacity: 0.4; -webkit-transition: all .3s linear; @@ -29,7 +32,8 @@ ul.sections li [class*=" icon-"]:before{ } ul.sections:hover li [class^="icon-"]:before, -ul.sections:hover li [class*=" icon-"]:before { +ul.sections:hover li [class*=" icon-"]:before, +ul.sections:hover li img.icon-section { opacity: 1 } @@ -65,8 +69,8 @@ ul.sections:hover a span { // ------------------------- ul.sections li.avatar { - height: 67px; - padding: 30px 0 2px 0; + height: 75px; + padding: 22px 0 2px 0; text-align: center; margin: 0 0 0 -4px; border-bottom: 1px solid @grayDark; @@ -81,8 +85,8 @@ ul.sections li.avatar a { } ul.sections li.avatar a img { - border-radius: 16px; - width: 30px + border-radius: 50%; + width: 30px; } .faded ul.sections li { diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index 4e2e3e77e8..d809418f2c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -29,13 +29,22 @@ min-width: 100%; width: auto; } -.umb-tree li.current > div, .umb-tree div.selected { +.umb-tree li.current > div, +.umb-tree div.selected { background: @blue; } -.umb-tree li.current > div a.umb-options i, .umb-tree div.selected i { +.umb-tree li.current > div a.umb-options i, +.umb-tree div.selected i { background: #fff; border-color: @blue; + transition: opacity 120ms ease; } + +.umb-tree li.current > div a.umb-options:hover i, +.umb-tree div.selected i { + opacity: .7; +} + .umb-tree li.current > div a, .umb-tree li.current > div i.icon, .umb-tree li.current > div ins { @@ -43,13 +52,23 @@ background: @blue; border-color: @blue; } + .umb-tree li.root > div { - padding-left: 20px; + padding: 0; } - .umb-tree li.root > div h5 { - margin-top: 10px; + margin: 0; + width: 100%; + + display: flex; + align-items: center; +} + +.umb-tree li.root > div h5 > a, .umb-tree-header { + display: flex; + padding: 20px 0 20px 20px; + box-sizing: border-box; } .umb-tree * { @@ -68,9 +87,9 @@ } .umb-tree a { - vertical-align: middle; - display: inline-block; cursor:pointer; + text-decoration: none; + outline: none; } .umb-tree a:hover { @@ -78,10 +97,13 @@ } .umb-tree div { - vertical-align: middle; padding: 5px 0 5px 0; position: relative; overflow: hidden; + display: flex; + flex-wrap: nowrap; + align-items: center; + } .umb-tree a.noSpr { @@ -95,7 +117,8 @@ visibility: visible; } -.umb-tree li.root > div a, .umb-tree li.root h5, .umb-tree-header { +.umb-tree li.root > div a, +.umb-tree li.root h5, .umb-tree-header { text-transform: uppercase; color: #b3b3b3; font-weight: bold; @@ -103,14 +126,18 @@ } .umb-tree ins { - vertical-align: middle; margin: -4px 0 0 -16px; width: 16px; height: 16px; - display: inline-block; visibility: hidden; text-decoration: none; font-size: 12px; + + transition: opacity 120ms ease; +} + +.umb-tree ins:hover { + opacity: .7; } .umb-tree li:hover ins { @@ -118,12 +145,32 @@ cursor: pointer } +.umb-tree li div { + padding: 0; +} + +.umb-tree li > div a:not(.umb-options) { + padding: 6px 0; + width: 100%; + display: flex; +} + +.umb-tree li > div:hover a:not(.umb-options) { + overflow: hidden; + margin-right: 6px; +} + .umb-tree .icon { vertical-align: middle; - margin: 1px 13px 1px 0px; + margin: 0 13px 0 0; color: #21201C; font-size: 20px; } + +.umb-tree-icon { + cursor: pointer; +} + .umb-tree i.noSpr { display: inline-block; margin-top: 1px; @@ -147,11 +194,23 @@ /*color:@turquoise;*/ } +.umb-tree .umb-search-group-item { + padding-left: 20px; +} + +.umb-tree .umb-search-group-item-link { + display: flex; + flex-wrap: wrap; + flex-direction: column; +} + .icon-check:before { content: "\e165"; } -.umb-tree .umb-tree-node-checked i { +.umb-tree .umb-tree-node-checked i[class^="icon-"], +.umb-tree .umb-tree-node-checked i[class*=" icon-"] { + font-family: 'icomoon' !important; color:@blue !important; } .umb-tree .umb-tree-node-checked i:before { @@ -161,14 +220,14 @@ a.umb-options { visibility: hidden; - cursor: pointer; - display: inline-block; - text-align: center; - position: absolute; - right: 10px; - top: 3px; - padding: 0 3px 3px 5px; - border: 1px solid transparent; + + display: flex; + justify-content: flex-end; + + padding: 9px 5px; + text-align: center; + cursor: pointer; + margin-right: 10px; } a.umb-options i { @@ -180,14 +239,20 @@ a.umb-options i { margin: 0 2px 0 0; } +a.umb-options i:last-child { + margin: 0; +} + a.umb-options:hover { background: @btnBackgroundHighlight; - border: 1px solid @grayLight; .border-radius(@baseBorderRadius); } li.root > div > a.umb-options { - top: 13px; + top: 18px; + + display: flex; + padding: 10px 5px; } .hide-options a.umb-options{display: none !important} @@ -247,6 +312,7 @@ div.protected:before{ font-size: 20px; padding-left: 7px; padding-top: 7px; + bottom: 0; } div.has-unpublished-version:before{ @@ -257,6 +323,7 @@ div.has-unpublished-version:before{ font-size: 20px; padding-left: 7px; padding-top: 7px; + bottom: 0; } div.not-allowed > i.icon,div.not-allowed > a{ @@ -275,6 +342,8 @@ div.is-container:before{ font-size: 8px; padding-left: 13px; padding-top: 8px; + pointer-events: none; + bottom: 0; } div.locked:before{ @@ -285,6 +354,7 @@ div.locked:before{ font-size: 20px; padding-left: 7px; padding-top: 7px; + bottom: 0; } // Tree context menu @@ -294,11 +364,6 @@ div.locked:before{ padding: 0px; list-style: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; } @@ -306,19 +371,20 @@ div.locked:before{ } .umb-actions li.sep { - margin-top: 4px; display: block; border-top: 1px solid #efefef; } -.umb-actions li.sep a { - margin-top: 4px; + +.umb-actions li.sep:first-child { + border-top: none; } + .umb-actions a { white-space: nowrap; display: block; font-size: 14px; color: @black; - padding: 4px 25px 4px 20px; + padding: 8px 25px 8px 20px; text-decoration: none; cursor: pointer; } @@ -359,10 +425,9 @@ div.locked:before{ padding-left: 10px; } .umb-actions-child li .menu-label { - font-size: 12px; - margin-bottom: 10px; + font-size: 14px; color: #000; - margin-left: 20px; + margin-left: 10px; } .umb-actions-child li .menu-label small { @@ -378,6 +443,9 @@ div.locked:before{ } .umb-actions-child i { font-size: 32px; + min-width: 32px; + text-align: center; + line-height: 24px; /* set line-height to ensure all icons use same line-height */ } .umb-actions-child li.add { @@ -416,9 +484,12 @@ div.locked:before{ // ------------------------ .umb-tree li div.l{ -width:100%; -height:1px; -overflow:hidden; + width:100%; + height:1px; + overflow:hidden; + position: absolute; + left: 0; + bottom: 0; } .umb-tree li div.l div { @@ -433,15 +504,18 @@ overflow:hidden; /*body.touch .umb-tree .icon{font-size: 19px;}*/ body.touch .umb-tree ins{font-size: 14px; visibility: visible; padding: 7px;} -body.touch .umb-tree li div { +body.touch .umb-tree li > div { padding-top: 8px; padding-bottom: 8px; font-size: 110%; } -body.touch .umb-actions a{ - padding: 7px 25px 7px 20px; - font-size: 110%; +// change height of this if touch devices should have a different height of preloader. +body.touch .umb-tree li div.l div { + padding: 0; } -body.touch a.umb-options i {margin-top: 20px;} +body.touch .umb-actions a { + padding: 7px 25px 7px 20px; + font-size: 110%; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index ec73e62c0c..5bddbd8022 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -14,9 +14,10 @@ @grayDarker: #222; @grayDark: #343434; @gray: #555; +@grayMed: #999; @grayLight: #d9d9d9; @grayLighter: #f8f8f8; -@white: #fff; +@white: #fff; // Accent colors @@ -85,9 +86,11 @@ @paddingSmall: 2px 10px; // 26px @paddingMini: 0 6px; // 22px -@baseBorderRadius: 2px; -@borderRadiusLarge: 6px; -@borderRadiusSmall: 3px; + +// Disabled this to keep consistency throughout the backoffice UI. Untill a better solution is thought up, this will do. +@baseBorderRadius: 0px; // 2px; +@borderRadiusLarge: 0px; // 6px; +@borderRadiusSmall: 0px; // 3px; // Tables @@ -112,11 +115,11 @@ @btnSuccessBackground: #53a93f; @btnSuccessBackgroundHighlight: #53a93f; -@btnWarningBackground: lighten(@orange, 15%); +@btnWarningBackground: @orange; @btnWarningBackgroundHighlight: @orange; @btnDangerBackground: #ee5f5b; -@btnDangerBackgroundHighlight: #bd362f; +@btnDangerBackgroundHighlight: #ee5f5b; @btnInverseBackground: #444; @btnInverseBackgroundHighlight: @grayDarker; @@ -348,4 +351,4 @@ // SORTABLE // -------------------------------------------------- @sortableHelperBg: rgba(4, 156, 219, 0.5); -@sortablePlaceholderBg : @blue; \ No newline at end of file +@sortablePlaceholderBg : @blue; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/loader.dev.js b/src/Umbraco.Web.UI.Client/src/loader.dev.js index 2be6593238..6fa09f00d9 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.dev.js +++ b/src/Umbraco.Web.UI.Client/src/loader.dev.js @@ -1,51 +1,49 @@ -LazyLoad.js( - [ - 'lib/jquery/jquery.min.js', - 'lib/angular/1.1.5/angular.min.js', - 'lib/underscore/underscore-min.js', - - 'lib/jquery-ui/jquery-ui.min.js', - - 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.js', - 'lib/angular/1.1.5/angular-sanitize.min.js', - - 'lib/angular/angular-ui-sortable.js', - - 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', - - 'lib/blueimp-load-image/load-image.all.min.js', - 'lib/jquery-file-upload/jquery.fileupload.js', - 'lib/jquery-file-upload/jquery.fileupload-process.js', - 'lib/jquery-file-upload/jquery.fileupload-image.js', - 'lib/jquery-file-upload/jquery.fileupload-angular.js', - - 'lib/bootstrap/js/bootstrap.2.3.2.min.js', - 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', - 'lib/umbraco/Extensions.js', - - 'lib/umbraco/NamespaceManager.js', - 'lib/umbraco/LegacyUmbClientMgr.js', - 'lib/umbraco/LegacySpeechBubble.js', - - 'js/umbraco.servervariables.js', - 'js/app.dev.js', - 'js/umbraco.httpbackend.js', - 'js/umbraco.testing.js', - - 'js/umbraco.directives.js', - 'js/umbraco.filters.js', - 'js/umbraco.resources.js', - 'js/umbraco.services.js', - 'js/umbraco.security.js', - 'js/umbraco.controllers.js', - 'js/routes.js', - 'js/init.js' - ], - - function () { - jQuery(document).ready(function () { - angular.bootstrap(document, ['umbraco']); - }); - } +LazyLoad.js( + [ + 'lib/jquery/jquery.min.js', + 'lib/jquery-ui/jquery-ui.min.js', + + + /* 1.1.5 */ + 'lib/angular/1.1.5/angular.min.js', + 'lib/angular/1.1.5/angular-cookies.min.js', + + 'lib/angular/1.1.5/angular-sanitize.min.js', + + 'lib/angular/angular-ui-sortable.js', + + 'lib/angular/1.1.5/angular-mocks.js', + 'lib/angular/1.1.5/angular-mobile.min.js', + 'lib/underscore/underscore-min.js', + + 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', + + 'lib/bootstrap/js/bootstrap.2.3.2.min.js', + 'lib/bootstrap-tabdrop/bootstrap-tabdrop.min.js', + 'lib/umbraco/Extensions.js', + + 'lib/umbraco/NamespaceManager.js', + 'lib/umbraco/LegacyUmbClientMgr.js', + 'lib/umbraco/LegacySpeechBubble.js', + + 'js/umbraco.servervariables.js', + 'js/app.dev.js', + 'js/umbraco.httpbackend.js', + 'js/umbraco.testing.js', + + 'js/umbraco.directives.js', + 'js/umbraco.filters.js', + 'js/umbraco.resources.js', + 'js/umbraco.services.js', + 'js/umbraco.security.js', + 'js/umbraco.controllers.js', + 'js/routes.js', + 'js/init.js' + ], + + function () { + jQuery(document).ready(function () { + angular.bootstrap(document, ['umbraco']); + }); + } ); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 3f4b28597d..69e6779a15 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -1,11 +1,11 @@ app.config(function ($routeProvider) { - - /** This checks if the user is authenticated for a route and what the isRequired is set to. + + /** This checks if the user is authenticated for a route and what the isRequired is set to. Depending on whether isRequired = true, it first check if the user is authenticated and will resolve successfully otherwise the route will fail and the $routeChangeError event will execute, in that handler we will redirect to the rejected path that is resolved from this method and prevent default (prevent the route from executing) */ var canRoute = function(isRequired) { - + return { /** Checks that the user is authenticated, then ensures that are requires assets are loaded */ isAuthenticatedAndReady: function ($q, userService, $route, assetsService, appState) { @@ -17,12 +17,12 @@ app.config(function ($routeProvider) { deferred.resolve(true); return deferred.promise; } - + userService.isAuthenticated() .then(function () { assetsService._loadInitAssets().then(function() { - + //This could be the first time has loaded after the user has logged in, in this case // we need to broadcast the authenticated event - this will be handled by the startup (init) // handler to set/broadcast the ready state @@ -59,7 +59,7 @@ app.config(function ($routeProvider) { //this will resolve successfully so the route will continue deferred.resolve(true); } - }); + }); return deferred.promise; } }; @@ -85,18 +85,18 @@ app.config(function ($routeProvider) { $routeProvider .when('/login', { templateUrl: 'views/common/login.html', - //ensure auth is *not* required so it will redirect to / + //ensure auth is *not* required so it will redirect to / resolve: canRoute(false) }) .when('/login/:check', { templateUrl: 'views/common/login.html', - //ensure auth is *not* required so it will redirect to / + //ensure auth is *not* required so it will redirect to / resolve: canRoute(false) }) - .when('/logout', { - redirectTo: '/login/false', - resolve: doLogout() - }) + .when('/logout', { + redirectTo: '/login/false', + resolve: doLogout() + }) .when('/:section', { templateUrl: function (rp) { if (rp.section.toLowerCase() === "default" || rp.section.toLowerCase() === "umbraco" || rp.section === "") @@ -118,7 +118,7 @@ app.config(function ($routeProvider) { return 'views/common/legacy.html'; }, resolve: canRoute(true) - }) + }) .when('/:section/:tree/:method', { templateUrl: function (rp) { if (!rp.method) @@ -130,7 +130,7 @@ app.config(function ($routeProvider) { // angular dashboards working. Perhaps a normal section dashboard would list out the registered // dashboards (as tabs if we wanted) and each tab could actually be a route link to one of these views? - return 'views/' + rp.tree + '/' + rp.method + '.html'; + return ('views/' + rp.tree + '/' + rp.method + '.html'); }, resolve: canRoute(true) }) @@ -147,27 +147,27 @@ app.config(function ($routeProvider) { // Here we need to figure out if this route is for a package tree and if so then we need // to change it's convention view path to: // /App_Plugins/{mypackage}/backoffice/{treetype}/{method}.html - + // otherwise if it is a core tree we use the core paths: // views/{treetype}/{method}.html var packageTreeFolder = treeService.getTreePackageFolder($routeParams.tree); if (packageTreeFolder) { - $scope.templateUrl = Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + + $scope.templateUrl = (Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + "/" + packageTreeFolder + - "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"; + "/backoffice/" + $routeParams.tree + "/" + $routeParams.method + ".html"); } else { - $scope.templateUrl = 'views/' + $routeParams.tree + '/' + $routeParams.method + '.html'; + $scope.templateUrl = ('views/' + $routeParams.tree + '/' + $routeParams.method + '.html'); } - - }, + + }, resolve: canRoute(true) - }) + }) .otherwise({ redirectTo: '/login' }); }).config(function ($locationProvider) { //$locationProvider.html5Mode(false).hashPrefix('!'); //turn html5 mode off // $locationProvider.html5Mode(true); //turn html5 mode on -}); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js index 006146095a..e722817959 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.controller.js @@ -9,6 +9,11 @@ */ function DashboardController($scope, $routeParams, dashboardResource, localizationService) { + + $scope.page = {}; + $scope.page.nameLocked = true; + $scope.page.loading = true; + $scope.dashboard = {}; localizationService.localize("sections_" + $routeParams.section).then(function(name){ $scope.dashboard.name = name; @@ -16,9 +21,10 @@ function DashboardController($scope, $routeParams, dashboardResource, localizati dashboardResource.getDashboard($routeParams.section).then(function(tabs){ $scope.dashboard.tabs = tabs; + $scope.page.loading = false; }); } //register it -angular.module('umbraco').controller("Umbraco.DashboardController", DashboardController); \ No newline at end of file +angular.module('umbraco').controller("Umbraco.DashboardController", DashboardController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html index 0ee402a63f..3136bc28e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html @@ -1,31 +1,51 @@ -
      - - -
      -

      {{dashboard.name}}

      -
      -
      - - -
      -
      +
      -
      -

      {{property.caption}}

      -
      -
      + -
      -

      {{property.caption}}

      - - -
      + -
      -
      - - - - \ No newline at end of file + + + + + + + + + + +
      + +
      +

      {{property.caption}}

      +
      +
      + +
      +

      {{property.caption}}

      + + +
      + +
      + +
      +
      + +
      + +
      + +  +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/help.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/help.html index f066da8e7c..b9428fb242 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/help.html @@ -37,7 +37,7 @@
    • 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 de71977ebe..ec1ad6e663 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,13 +7,14 @@ angular.module("umbraco") $scope.icons = icons; }); - $scope.submitClass = function(icon){ - if($scope.color) - { + $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 bcf7fe3247..96ab990447 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 @@ -2,55 +2,49 @@
    + @@ -144,4 +156,4 @@ - \ No newline at end of file + 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 1020fdaed2..48302cea02 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 @@ -4,7 +4,7 @@
    + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index 68579bb442..bac8fcded7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -1,105 +1,167 @@ -angular.module("umbraco") - .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource) { - - $scope.history = historyService.getCurrent(); - $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; - var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.remove", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { - $scope.history = []; - })); - - $scope.logout = function () { - - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { - //one time listener, remove the event - pendingChangeEvent(); - $scope.close(); - }); - - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.close(); - $location.path("/logout"); - }; - - $scope.gotoHistory = function (link) { - $location.path(link); - $scope.close(); - }; - - //Manually update the remaining timeout seconds - function updateTimeout() { - $timeout(function () { - if ($scope.remainingAuthSeconds > 0) { - $scope.remainingAuthSeconds--; - $scope.$digest(); - //recurse - updateTimeout(); - } - - }, 1000, false); // 1 second, do NOT execute a global digest - } - - function updateUserInfo() { - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - authResource.getCurrentUserLinkedLogins().then(function(logins) { - //reset all to be un-linked - for (var provider in $scope.externalLoginProviders) { - $scope.externalLoginProviders[provider].linkedProviderKey = undefined; - } - - //set the linked logins - for (var login in logins) { - var found = _.find($scope.externalLoginProviders, function (i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = logins[login]; - } - } - }); - } - }); - } - - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); - if (!result) { - e.preventDefault(); - return; - } - - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - updateUserInfo(); - }); - } - - updateUserInfo(); - - //remove all event handlers - $scope.$on('$destroy', function () { - for (var e = 0; e < evts.length; e++) { - evts[e](); - } - - }); - - }); \ No newline at end of file +angular.module("umbraco") + .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper) { + + $scope.history = historyService.getCurrent(); + $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.showPasswordFields = false; + $scope.changePasswordButtonState = "init"; + + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on("historyService.add", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.remove", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.removeAll", function (e, args) { + $scope.history = []; + })); + + $scope.logout = function () { + + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + $scope.close(); + }); + + + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + $scope.close(); + $location.path("/logout"); + }; + + $scope.gotoHistory = function (link) { + $location.path(link); + $scope.close(); + }; + + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if ($scope.remainingAuthSeconds > 0) { + $scope.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); + } + + }, 1000, false); // 1 second, do NOT execute a global digest + } + + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + } + + updateUserInfo(); + + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + + }); + + /* ---------- UPDATE PASSWORD ---------- */ + + //create the initial model for change password property editor + $scope.changePasswordModel = { + alias: "_umb_password", + view: "changepassword", + config: {}, + value: {} + }; + + //go get the config for the membership provider and add it to the model + currentUserResource.getMembershipProviderConfig().then(function(data) { + $scope.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + $scope.changePasswordModel.config.hasPassword = true; + $scope.changePasswordModel.config.disableToggle = true; + $scope.changePasswordModel.config.enableReset = false; + }); + + $scope.changePassword = function() { + + if (formHelper.submitForm({ scope: $scope })) { + + $scope.changePasswordButtonState = "busy"; + + currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { + + //if the password has been reset, then update our model + if (data.value) { + $scope.changePasswordModel.value.generatedPassword = data.value; + } + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + $scope.changePasswordButtonState = "success"; + + }, function (err) { + + formHelper.handleError(err); + + $scope.changePasswordButtonState = "error"; + + }); + + } + + }; + + $scope.togglePasswordFields = function() { + clearPasswordFields(); + $scope.showPasswordFields = !$scope.showPasswordFields; + } + + function clearPasswordFields() { + $scope.changePasswordModel.value.newPassword = ""; + $scope.changePasswordModel.confirm = ""; + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index 8346acfabb..83be1a7782 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -8,6 +8,7 @@

    {{user.name}}

    + Umbraco version {{version}}

    : {{remainingAuthSeconds | timespan}} @@ -19,18 +20,27 @@

    -
    -
    -

    - - Edit - -

    +
    + +
    + + + Edit + + + + +
    -
    +
    External login providers
    @@ -63,7 +73,7 @@
    -
    +
    • @@ -74,8 +84,36 @@
    -
    +
    + +
    Change password
    + +
    + + + + + + + + + +
    + +
    - Umbraco version {{version}}
    -
    \ No newline at end of file +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html index 97be02baea..0b2926307c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/notifications/confirmroutechange.html @@ -1,7 +1,7 @@
    -

    You have unsaved changes

    -

    Are you sure you want to navigate away from this page? - you have unsaved changes

    +

    You have unsaved changes

    +

    Are you sure you want to navigate away from this page? - you have unsaved changes

    - - + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html new file mode 100644 index 0000000000..8e70ad163c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html @@ -0,0 +1,34 @@ +
    + +
    + + +
    + + + + +
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js new file mode 100644 index 0000000000..7a908a7260 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js @@ -0,0 +1,19 @@ + (function() { + "use strict"; + + function CompositionsOverlay($scope) { + + var vm = this; + + vm.isSelected = isSelected; + + function isSelected(alias) { + if($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { + return true; + } + } + } + + angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); + +})(); 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 new file mode 100644 index 0000000000..6554a0377f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -0,0 +1,55 @@ +
    + +
    + +
    + +
    + +
    + + + + + + + + +
      +
    • + +
      + +
      + + + +
    • +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorpicker/editorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorpicker/editorpicker.controller.js new file mode 100644 index 0000000000..57043e39c9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorpicker/editorpicker.controller.js @@ -0,0 +1,213 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ + +(function() { + "use strict"; + + function EditorPickerOverlay($scope, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService) { + + var vm = this; + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectEditor"); + } + + vm.searchTerm = ""; + vm.showTabs = false; + vm.tabsLoaded = 0; + vm.typesAndEditors = []; + vm.userConfigured = []; + vm.loading = false; + vm.tabs = [{ + active: true, + id: 1, + label: localizationService.localize("contentTypeEditor_availableEditors"), + alias: "Default", + typesAndEditors: [] + }, { + active: false, + id: 2, + label: localizationService.localize("contentTypeEditor_reuse"), + alias: "Reuse", + userConfigured: [] + }]; + + vm.filterItems = filterItems; + vm.showDetailsOverlay = showDetailsOverlay; + vm.hideDetailsOverlay = hideDetailsOverlay; + vm.pickEditor = pickEditor; + vm.pickDataType = pickDataType; + + function activate() { + + getGroupedDataTypes(); + getGroupedPropertyEditors(); + + } + + function getGroupedPropertyEditors() { + + vm.loading = true; + + dataTypeResource.getGroupedPropertyEditors().then(function(data) { + vm.tabs[0].typesAndEditors = data; + vm.typesAndEditors = data; + vm.tabsLoaded = vm.tabsLoaded + 1; + checkIfTabContentIsLoaded(); + }); + + } + + function getGroupedDataTypes() { + + vm.loading = true; + + dataTypeResource.getGroupedDataTypes().then(function(data) { + vm.tabs[1].userConfigured = data; + vm.userConfigured = data; + vm.tabsLoaded = vm.tabsLoaded + 1; + checkIfTabContentIsLoaded(); + }); + + } + + function checkIfTabContentIsLoaded() { + if (vm.tabsLoaded === 2) { + vm.loading = false; + vm.showTabs = true; + } + } + + function filterItems() { + // clear item details + $scope.model.itemDetails = null; + + if (vm.searchTerm) { + vm.showFilterResult = true; + vm.showTabs = false; + } else { + vm.showFilterResult = false; + vm.showTabs = true; + } + + } + + function showDetailsOverlay(property) { + + var propertyDetails = {}; + propertyDetails.icon = property.icon; + propertyDetails.title = property.name; + + $scope.model.itemDetails = propertyDetails; + + } + + function hideDetailsOverlay() { + $scope.model.itemDetails = null; + } + + function pickEditor(editor) { + + var parentId = -1; + + dataTypeResource.getScaffold(parentId).then(function(dataType) { + + // set alias + dataType.selectedEditor = editor.alias; + + // set name + var nameArray = []; + + if ($scope.model.contentTypeName) { + nameArray.push($scope.model.contentTypeName); + } + + if ($scope.model.property.label) { + nameArray.push($scope.model.property.label); + } + + if (editor.name) { + nameArray.push(editor.name); + } + + // make name + dataType.name = nameArray.join(" - "); + + // get pre values + dataTypeResource.getPreValues(dataType.selectedEditor).then(function(preValues) { + + dataType.preValues = preValues; + + openEditorSettingsOverlay(dataType, true); + + }); + + }); + + } + + function pickDataType(selectedDataType) { + + dataTypeResource.getById(selectedDataType.id).then(function(dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { + submitOverlay(dataType, propertyType, false); + }); + }); + + } + + function openEditorSettingsOverlay(dataType, isNew) { + vm.editorSettingsOverlay = { + title: localizationService.localize("contentTypeEditor_editorSettings"), + dataType: dataType, + view: "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html", + show: true, + submit: function(model) { + var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); + + dataTypeResource.save(model.dataType, preValues, isNew).then(function(newDataType) { + + contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { + + submitOverlay(newDataType, propertyType, true); + + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + + }); + + }); + } + }; + + } + + function submitOverlay(dataType, propertyType, isNew) { + + // update property + $scope.model.property.config = propertyType.config; + $scope.model.property.editor = propertyType.editor; + $scope.model.property.view = propertyType.view; + $scope.model.property.dataTypeId = dataType.id; + $scope.model.property.dataTypeIcon = dataType.icon; + $scope.model.property.dataTypeName = dataType.name; + + $scope.model.updateSameDataTypes = isNew; + + $scope.model.submit($scope.model); + + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.Overlays.EditorPickerOverlay", EditorPickerOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html new file mode 100644 index 0000000000..392d45eb55 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html @@ -0,0 +1,117 @@ +
    + + +
    + +
    + + + + +
    + + + + + + + +
    +
    +
    {{key}}
    + +
    +
    + +
    +
    +
    {{key}}
    + +
    +
    + +
    + +
    + +
    + + +
    + +
    +
    +
    +
    {{key}}
    + +
    +
    + +
    +
    +
    +
    {{key}}
    + +
    +
    + +
    + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html new file mode 100644 index 0000000000..0ea012bd50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html @@ -0,0 +1,21 @@ +
    + + using this editor will get updated with the new settings. +
    + +
    +
    + +
    + +
    +
    +
    + +
    + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.controller.js new file mode 100644 index 0000000000..78ae77b46c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.controller.js @@ -0,0 +1,202 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ + + (function() { + "use strict"; + + function PropertySettingsOverlay($scope, contentTypeResource, dataTypeResource, dataTypeHelper, localizationService) { + + var vm = this; + + vm.showValidationPattern = false; + vm.focusOnPatternField = false; + vm.focusOnMandatoryField = false; + vm.selectedValidationType = {}; + vm.validationTypes = [ + { + "name": localizationService.localize("validation_validateAsEmail"), + "key": "email", + "pattern": "[a-zA-Z0-9_\.\+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-\.]+", + "enableEditing": true + }, + { + "name": localizationService.localize("validation_validateAsNumber"), + "key": "number", + "pattern": "^[0-9]*$", + "enableEditing": true + }, + { + "name": localizationService.localize("validation_validateAsUrl"), + "key": "url", + "pattern": "https?\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}", + "enableEditing": true + }, + { + "name": localizationService.localize("validation_enterCustomValidation"), + "key": "custom", + "pattern": "", + "enableEditing": true + } + ]; + + vm.changeValidationType = changeValidationType; + vm.changeValidationPattern = changeValidationPattern; + vm.openEditorPickerOverlay = openEditorPickerOverlay; + vm.openEditorSettingsOverlay = openEditorSettingsOverlay; + + function activate() { + + matchValidationType(); + + } + + function changeValidationPattern() { + matchValidationType(); + } + + function openEditorPickerOverlay(property) { + + vm.focusOnMandatoryField = false; + + vm.editorPickerOverlay = {}; + vm.editorPickerOverlay.property = $scope.model.property; + vm.editorPickerOverlay.contentTypeName = $scope.model.contentTypeName; + vm.editorPickerOverlay.view = "views/common/overlays/contenttypeeditor/editorpicker/editorpicker.html"; + vm.editorPickerOverlay.show = true; + + vm.editorPickerOverlay.submit = function(model) { + + $scope.model.updateSameDataTypes = model.updateSameDataTypes; + + vm.focusOnMandatoryField = true; + + // update property + property.config = model.property.config; + property.editor = model.property.editor; + property.view = model.property.view; + property.dataTypeId = model.property.dataTypeId; + property.dataTypeIcon = model.property.dataTypeIcon; + property.dataTypeName = model.property.dataTypeName; + + vm.editorPickerOverlay.show = false; + vm.editorPickerOverlay = null; + }; + + vm.editorPickerOverlay.close = function(model) { + vm.editorPickerOverlay.show = false; + vm.editorPickerOverlay = null; + }; + + } + + function openEditorSettingsOverlay(property) { + + vm.focusOnMandatoryField = false; + + // get data type + dataTypeResource.getById(property.dataTypeId).then(function(dataType) { + + vm.editorSettingsOverlay = {}; + vm.editorSettingsOverlay.title = "Editor settings"; + vm.editorSettingsOverlay.view = "views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html"; + vm.editorSettingsOverlay.dataType = dataType; + vm.editorSettingsOverlay.show = true; + + vm.editorSettingsOverlay.submit = function(model) { + + var preValues = dataTypeHelper.createPreValueProps(model.dataType.preValues); + + dataTypeResource.save(model.dataType, preValues, false).then(function(newDataType) { + + contentTypeResource.getPropertyTypeScaffold(newDataType.id).then(function(propertyType) { + + // update editor + property.config = propertyType.config; + property.editor = propertyType.editor; + property.view = propertyType.view; + property.dataTypeId = newDataType.id; + property.dataTypeIcon = newDataType.icon; + property.dataTypeName = newDataType.name; + + // set flag to update same data types + $scope.model.updateSameDataTypes = true; + + vm.focusOnMandatoryField = true; + + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + + }); + + }); + + }; + + vm.editorSettingsOverlay.close = function(oldModel) { + vm.editorSettingsOverlay.show = false; + vm.editorSettingsOverlay = null; + }; + + }); + + } + + function matchValidationType() { + + if($scope.model.property.validation.pattern !== null && $scope.model.property.validation.pattern !== "" && $scope.model.property.validation.pattern !== undefined) { + + var match = false; + + // find and show if a match from the list has been chosen + angular.forEach(vm.validationTypes, function(validationType, index){ + if($scope.model.property.validation.pattern === validationType.pattern) { + vm.selectedValidationType = vm.validationTypes[index]; + vm.showValidationPattern = true; + match = true; + } + }); + + // if there is no match - choose the custom validation option. + if(!match) { + angular.forEach(vm.validationTypes, function(validationType){ + if(validationType.key === "custom") { + vm.selectedValidationType = validationType; + vm.showValidationPattern = true; + } + }); + } + } + + } + + function changeValidationType(selectedValidationType) { + + if(selectedValidationType) { + $scope.model.property.validation.pattern = selectedValidationType.pattern; + vm.showValidationPattern = true; + + // set focus on textarea + if(selectedValidationType.key === "custom") { + vm.focusOnPatternField = true; + } + + } else { + $scope.model.property.validation.pattern = ""; + vm.showValidationPattern = false; + } + + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.Overlay.PropertySettingsOverlay", PropertySettingsOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html new file mode 100644 index 0000000000..7c457ac4c9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html @@ -0,0 +1,118 @@ +
    + +
    +
    + +
    Required label
    +
    +
    + +
    +
    + +
    + +
    + + + +
    + +
    + + + + + + + +
    + +
    + +
    + + + + + +
    + + + + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/copy/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/copy/copy.controller.js new file mode 100644 index 0000000000..5d3ea998d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/copy/copy.controller.js @@ -0,0 +1,118 @@ + (function() { + "use strict"; + + function CopyOverlay($scope, localizationService, eventsService) { + + var vm = this; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_copy"); + } + + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + + var dialogOptions = $scope.model; + var searchText = "Search..."; + var node = dialogOptions.currentNode; + + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + $scope.model.relateToOriginal = true; + $scope.dialogTreeEventHandler = $({}); + + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + vm.searchInfo.showSearch = true; + vm.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + vm.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + //eventsService.emit("editors.content.copyController.select", args); + + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; + } + + $scope.model.target = args.node; + $scope.model.target.selected = true; + } + + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } + + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + } + + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + + + } + + angular.module("umbraco").controller("Umbraco.Overlays.CopyOverlay", CopyOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/copy/copy.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/copy/copy.html new file mode 100644 index 0000000000..adb57b4af0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/copy/copy.html @@ -0,0 +1,41 @@ +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/embed/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/embed/embed.controller.js new file mode 100644 index 0000000000..6fae3cd1d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/embed/embed.controller.js @@ -0,0 +1,102 @@ +(function() { + "use strict"; + + function EmbedOverlay($scope, $http, umbRequestHelper, localizationService) { + + var vm = this; + var origWidth = 500; + var origHeight = 300; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_embed"); + } + + $scope.model.embed = { + url: "", + width: 360, + height: 240, + constrain: true, + preview: "", + success: false, + info: "", + supportsDimensions: "" + }; + + vm.showPreview = showPreview; + vm.changeSize = changeSize; + + function showPreview() { + + if ($scope.model.embed.url) { + $scope.model.embed.show = true; + $scope.model.embed.preview = "
    "; + $scope.model.embed.info = ""; + $scope.model.embed.success = false; + + $http({ + method: 'GET', + url: umbRequestHelper.getApiUrl("embedApiBaseUrl", "GetEmbed"), + params: { + url: $scope.model.embed.url, + width: $scope.model.embed.width, + height: $scope.model.embed.height + } + }) + .success(function(data) { + + $scope.model.embed.preview = ""; + + switch (data.Status) { + case 0: + //not supported + $scope.model.embed.info = "Not supported"; + break; + case 1: + //error + $scope.model.embed.info = "Computer says no"; + break; + case 2: + $scope.model.embed.preview = data.Markup; + $scope.model.embed.supportsDimensions = data.SupportsDimensions; + $scope.model.embed.success = true; + break; + } + }) + .error(function() { + $scope.model.embed.supportsDimensions = false; + $scope.model.embed.preview = ""; + $scope.model.embed.info = "Computer says no"; + }); + } else { + $scope.model.embed.supportsDimensions = false; + $scope.model.embed.preview = ""; + $scope.model.embed.info = "Please enter a URL"; + } + } + + function changeSize(type) { + + var width, height; + + if ($scope.model.embed.constrain) { + width = parseInt($scope.model.embed.width, 10); + height = parseInt($scope.model.embed.height, 10); + if (type == 'width') { + origHeight = Math.round((width / origWidth) * height); + $scope.model.embed.height = origHeight; + } else { + origWidth = Math.round((height / origHeight) * width); + $scope.model.embed.width = origWidth; + } + } + if ($scope.model.embed.url !== "") { + showPreview(); + } + + } + + } + + angular.module("umbraco").controller("Umbraco.Overlays.EmbedOverlay", EmbedOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/embed/embed.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/embed/embed.html new file mode 100644 index 0000000000..22088a2a1a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/embed/embed.html @@ -0,0 +1,27 @@ +
    + + + + + + + +

    +
    +
    + +
    + + + + + + + + + + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/help/help.controller.js new file mode 100644 index 0000000000..e61a8e6aa6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/help/help.controller.js @@ -0,0 +1,56 @@ +angular.module("umbraco") + .controller("Umbraco.Overlays.HelpController", function ($scope, $location, $routeParams, helpService, userService, localizationService) { + $scope.section = $routeParams.section; + $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.model.subtitle = "Umbraco version" + " " + $scope.version; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_help"); + } + + if(!$scope.section){ + $scope.section = "content"; + } + + $scope.sectionName = $scope.section; + + var rq = {}; + rq.section = $scope.section; + + //translate section name + localizationService.localize("sections_" + rq.section).then(function (value) { + $scope.sectionName = value; + }); + + userService.getCurrentUser().then(function(user){ + + rq.usertype = user.userType; + rq.lang = user.locale; + + 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.findHelp(rq).then(function(topics){ + $scope.topics = topics; + }); + + helpService.findVideos(rq).then(function(videos){ + $scope.videos = videos; + }); + + }); + + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/help/help.html new file mode 100644 index 0000000000..ecfdd24828 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/help/help.html @@ -0,0 +1,50 @@ +
    + +
    +
    Help topics for: {{sectionName}}
    + + +
    + +
    +
    Video chapters for: {{sectionName}}
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js new file mode 100644 index 0000000000..bd2071a012 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.controller.js @@ -0,0 +1,31 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ +function IconPickerOverlay($scope, iconHelper, localizationService) { + + $scope.loading = true; + $scope.model.hideSubmitButton = true; + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectIcon"); + } + + iconHelper.getIcons().then(function(icons) { + $scope.icons = icons; + $scope.loading = false; + }); + + $scope.selectIcon = function(icon, color) { + $scope.model.icon = icon; + $scope.model.color = color; + $scope.submitForm($scope.model); + }; + +} + +angular.module("umbraco").controller("Umbraco.Overlays.IconPickerOverlay", IconPickerOverlay); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html new file mode 100644 index 0000000000..0091289e6e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/iconpicker/iconpicker.html @@ -0,0 +1,57 @@ +
    + +
    + +
    + +
    + +
    + + + +
    + +
    + + + No icons were found. + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js new file mode 100644 index 0000000000..a848c0ae90 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.controller.js @@ -0,0 +1,16 @@ +function ItemPickerOverlay($scope, localizationService) { + + if (!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectItem"); + } + + $scope.model.hideSubmitButton = true; + + $scope.selectItem = function(item) { + $scope.model.selectedItem = item; + $scope.submitForm($scope.model); + }; + +} + +angular.module("umbraco").controller("Umbraco.Overlays.ItemPickerOverlay", ItemPickerOverlay); 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 new file mode 100644 index 0000000000..02c68e9baf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -0,0 +1,26 @@ +
    + + + + + +
    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 new file mode 100644 index 0000000000..5d638444c7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -0,0 +1,157 @@ +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", + function ($scope, eventsService, dialogService, entityResource, contentResource, mediaHelper, userService, localizationService) { + var dialogOptions = $scope.model; + + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); + } + + $scope.dialogTreeEventHandler = $({}); + $scope.model.target = {}; + $scope.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + if (dialogOptions.currentTarget) { + $scope.model.target = dialogOptions.currentTarget; + + //if we have a node ID, we fetch the current node to build the form data + if ($scope.model.target.id) { + + if (!$scope.model.target.path) { + entityResource.getPath($scope.model.target.id, "Document").then(function (path) { + $scope.model.target.path = path; + //now sync the tree to this path + $scope.dialogTreeEventHandler.syncTree({ path: $scope.model.target.path, tree: "content" }); + }); + } + + contentResource.getNiceUrl($scope.model.target.id).then(function (url) { + $scope.model.target.url = url; + }); + } + } + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + eventsService.emit("dialogs.linkPicker.select", args); + + if ($scope.currentNode) { + //un-select if there's a current one selected + $scope.currentNode.selected = false; + } + + $scope.currentNode = args.node; + $scope.currentNode.selected = true; + $scope.model.target.id = args.node.id; + $scope.model.target.name = args.node.name; + + if (args.node.id < 0) { + $scope.model.target.url = "/"; + } + else { + contentResource.getNiceUrl(args.node.id).then(function (url) { + $scope.model.target.url = url; + }); + } + + if (!angular.isUndefined($scope.model.target.isMedia)) { + delete $scope.model.target.isMedia; + } + } + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + $scope.switchToMediaPicker = function () { + userService.getCurrentUser().then(function (userData) { + $scope.mediaPickerOverlay = { + view: "mediapicker", + startNodeId: userData.startMediaId, + show: true, + submit: function(model) { + var media = model.selectedImages[0]; + + $scope.model.target.id = media.id; + $scope.model.target.isMedia = true; + $scope.model.target.name = media.name; + $scope.model.target.url = mediaHelper.resolveFile(media); + + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + }); + }; + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = null; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, {event: evt, node: result}); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + }); 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 new file mode 100644 index 0000000000..aba5818e01 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html @@ -0,0 +1,86 @@ +
    + + + + + + + + + + + + + +
    +
    + Link to page +
    + + + + +
    + + + + +
    + + +
    + +
    + +
    +
    + Link to media +
    + + Select media + +
    + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js new file mode 100644 index 0000000000..e46bd26513 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.controller.js @@ -0,0 +1,116 @@ +function MacroPickerController($scope, entityResource, macroResource, umbPropEditorHelper, macroService, formHelper, localizationService) { + + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMacro"); + } + + $scope.macros = []; + $scope.model.selectedMacro = null; + $scope.wizardStep = "macroSelect"; + $scope.model.macroParams = []; + $scope.noMacroParams = false; + + $scope.changeMacro = function() { + if ($scope.wizardStep === "macroSelect") { + editParams(); + } else { + submitForm(); + } + }; + + /** changes the view to edit the params of the selected macro */ + function editParams() { + //get the macro params if there are any + macroResource.getMacroParameters($scope.model.selectedMacro.id) + .then(function (data) { + + //go to next page if there are params otherwise we can just exit + if (!angular.isArray(data) || data.length === 0) { + + $scope.noMacroParams = true; + + } else { + $scope.wizardStep = "paramSelect"; + $scope.model.macroParams = data; + + //fill in the data if we are editing this macro + if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroParamsDictionary) { + _.each($scope.model.dialogData.macroData.macroParamsDictionary, function (val, key) { + var prop = _.find($scope.model.macroParams, function (item) { + return item.alias == key; + }); + if (prop) { + + if (_.isString(val)) { + //we need to unescape values as they have most likely been escaped while inserted + val = _.unescape(val); + + //detect if it is a json string + if (val.detectIsJson()) { + try { + //Parse it to json + prop.value = angular.fromJson(val); + } + catch (e) { + // not json + prop.value = val; + } + } + else { + prop.value = val; + } + } + else { + prop.value = val; + } + } + }); + + } + } + + }); + } + + //here we check to see if we've been passed a selected macro and if so we'll set the + //editor to start with parameter editing + if ($scope.model.dialogData && $scope.model.dialogData.macroData) { + $scope.wizardStep = "paramSelect"; + } + + //get the macro list - pass in a filter if it is only for rte + entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) + .then(function (data) { + + //if 'allowedMacros' is specified, we need to filter + if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { + $scope.macros = _.filter(data, function(d) { + return _.contains($scope.model.dialogData.allowedMacros, d.alias); + }); + } + else { + $scope.macros = data; + } + + + //check if there's a pre-selected macro and if it exists + if ($scope.model.dialogData && $scope.model.dialogData.macroData && $scope.model.dialogData.macroData.macroAlias) { + var found = _.find(data, function (item) { + return item.alias === $scope.model.dialogData.macroData.macroAlias; + }); + if (found) { + //select the macro and go to next screen + $scope.model.selectedMacro = found; + editParams(); + return; + } + } + //we don't have a pre-selected macro so ensure the correct step is set + $scope.wizardStep = "macroSelect"; + }); + + +} + +angular.module("umbraco").controller("Umbraco.Overlays.MacroPickerController", MacroPickerController); 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 new file mode 100644 index 0000000000..318fed41a8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html @@ -0,0 +1,42 @@ +
    + +
    + + + + + + + +
    + +
    {{model.selectedMacro.name}}
    + +
      +
    • + + + + + + + +
    • +
    + + + 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 new file mode 100644 index 0000000000..774e14f50e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -0,0 +1,263 @@ +//used for the media picker dialog +angular.module("umbraco") + .controller("Umbraco.Overlays.MediaPickerController", + function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, eventsService, treeService, $element, $timeout, $cookies, $cookieStore, localizationService) { + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); + } + + var dialogOptions = $scope.model; + + $scope.disableFolderSelect = dialogOptions.disableFolderSelect; + $scope.onlyImages = dialogOptions.onlyImages; + $scope.showDetails = dialogOptions.showDetails; + $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; + $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; + $scope.cropSize = dialogOptions.cropSize; + $scope.lastOpenedNode = $cookieStore.get("umbLastOpenedMediaNodeId"); + if($scope.onlyImages){ + $scope.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); + } + else { + $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); + } + $scope.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + + $scope.model.selectedImages = []; + + //preload selected item + $scope.target = undefined; + if(dialogOptions.currentTarget){ + $scope.target = dialogOptions.currentTarget; + } + + $scope.upload = function(v){ + angular.element(".umb-file-dropzone-directive .file-select").click(); + }; + + $scope.dragLeave = function(el, event){ + $scope.activeDrag = false; + }; + + $scope.dragEnter = function(el, event){ + $scope.activeDrag = true; + }; + + $scope.submitFolder = function() { + + if ($scope.newFolderName) { + + mediaResource + .addFolder($scope.newFolderName, $scope.currentFolder.id) + .then(function(data) { + + //we've added a new folder so lets clear the tree cache for that specific item + treeService.clearCache({ + cacheKey: "__media", //this is the main media tree cache key + childrenOf: data.parentId //clear the children of the parent + }); + + $scope.gotoFolder(data); + + $scope.showFolderInput = false; + + $scope.newFolderName = ""; + + }); + + } else { + $scope.showFolderInput = false; + } + + }; + + $scope.enterSubmitFolder = function(event) { + if (event.keyCode === 13) { + $scope.submitFolder(); + event.stopPropagation(); + } + }; + + $scope.gotoFolder = function(folder) { + + if(!$scope.multiPicker) { + deselectAllImages($scope.model.selectedImages); + } + + if(!folder){ + folder = {id: -1, name: "Media", icon: "icon-folder"}; + } + + if (folder.id > 0) { + entityResource.getAncestors(folder.id, "media") + .then(function(anc) { + // anc.splice(0,1); + $scope.path = _.filter(anc, function (f) { + return f.path.indexOf($scope.startNodeId) !== -1; + }); + }); + } + else { + $scope.path = []; + } + + //mediaResource.rootMedia() + mediaResource.getChildren(folder.id) + .then(function(data) { + $scope.searchTerm = ""; + $scope.images = data.items ? data.items : []; + + // set already selected images to selected + for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { + + var folderImage = $scope.images[folderImageIndex]; + var imageIsSelected = false; + + for (var selectedImageIndex = 0; selectedImageIndex < $scope.model.selectedImages.length; selectedImageIndex++) { + var selectedImage = $scope.model.selectedImages[selectedImageIndex]; + + if(folderImage.key === selectedImage.key) { + imageIsSelected = true; + } + } + + if(imageIsSelected) { + folderImage.selected = true; + } + } + + }); + + $scope.currentFolder = folder; + + // for some reason i cannot set cookies with cookieStore + document.cookie="umbLastOpenedMediaNodeId=" + folder.id; + + }; + + $scope.clickHandler = function(image, event, index) { + if (image.isFolder) { + if ($scope.disableFolderSelect) { + $scope.gotoFolder(image); + } else { + eventsService.emit("dialogs.mediaPicker.select", image); + selectImage(image); + } + + } else { + + eventsService.emit("dialogs.mediaPicker.select", image); + + if($scope.showDetails) { + $scope.target = image; + $scope.target.url = mediaHelper.resolveFile(image); + $scope.openDetailsDialog(); + } else { + selectImage(image); + } + + } + + }; + + $scope.clickItemName = function(item) { + if(item.isFolder) { + $scope.gotoFolder(item); + } + }; + + function selectImage(image) { + + if(image.selected) { + + for(var i = 0; $scope.model.selectedImages.length > i; i++) { + + var imageInSelection = $scope.model.selectedImages[i]; + if(image.key === imageInSelection.key) { + image.selected = false; + $scope.model.selectedImages.splice(i, 1); + } + } + + } else { + + if(!$scope.multiPicker) { + deselectAllImages($scope.model.selectedImages); + } + + image.selected = true; + $scope.model.selectedImages.push(image); + } + + } + + function deselectAllImages(images) { + for (var i = 0; i < images.length; i++) { + var image = images[i]; + image.selected = false; + } + images.length = 0; + } + + $scope.onUploadComplete = function () { + $scope.gotoFolder($scope.currentFolder); + }; + + $scope.onFilesQueue = function(){ + $scope.activeDrag = false; + }; + + //default root item + if (!$scope.target) { + + if($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { + + entityResource.getById($scope.lastOpenedNode, "media") + .then(function(node){ + + // make sure that las opened node is on the same path as start node + var nodePath = node.path.split(","); + + if(nodePath.indexOf($scope.startNodeId.toString()) !== -1) { + $scope.gotoFolder({id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder"}); + } else { + $scope.gotoFolder({id: $scope.startNodeId, name: "Media", icon: "icon-folder"}); + } + + }, function (err) { + $scope.gotoFolder({id: $scope.startNodeId, name: "Media", icon: "icon-folder"}); + }); + + } else { + + $scope.gotoFolder({id: $scope.startNodeId, name: "Media", icon: "icon-folder"}); + + } + + } + + $scope.openDetailsDialog = function() { + + $scope.mediaPickerDetailsOverlay = {}; + $scope.mediaPickerDetailsOverlay.show = true; + + $scope.mediaPickerDetailsOverlay.submit = function(model) { + + $scope.model.selectedImages.push($scope.target); + $scope.model.submit($scope.model); + + $scope.mediaPickerDetailsOverlay.show = false; + $scope.mediaPickerDetailsOverlay = null; + + }; + + $scope.mediaPickerDetailsOverlay.close = function(oldModel) { + $scope.mediaPickerDetailsOverlay.show = false; + $scope.mediaPickerDetailsOverlay = null; + }; + + }; + + + }); 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 new file mode 100644 index 0000000000..51f05fea00 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -0,0 +1,140 @@ +
    + +
    + +
    + + + +
    + + +
    + +
    + +
    + + +
    + + + + + + + +
    + + + +
    + +
    + + +
    + +
    + +
    + Preview +
    + + + + +
    + +
    + +
    + + +
    + +
    + + +
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/membergrouppicker/membergrouppicker.controller.js new file mode 100644 index 0000000000..e823b2cc44 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/membergrouppicker/membergrouppicker.controller.js @@ -0,0 +1,64 @@ +//used for the member picker dialog +angular.module("umbraco").controller("Umbraco.Overlays.MemberGroupPickerController", + function($scope, eventsService, entityResource, searchService, $log, localizationService) { + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMemberGroup"); + } + + $scope.dialogTreeEventHandler = $({}); + $scope.multiPicker = $scope.model.multiPicker; + + function activate() { + + if($scope.multiPicker) { + $scope.model.selectedMemberGroups = []; + } else { + $scope.model.selectedMemberGroup = ""; + } + + } + + function selectMemberGroup(id) { + $scope.model.selectedMemberGroup = id; + } + + function selectMemberGroups(id) { + $scope.model.selectedMemberGroups.push(id); + } + + /** Method used for selecting a node */ + function select(text, id) { + + if ($scope.model.multiPicker) { + selectMemberGroups(id); + } + else { + selectMemberGroup(id); + $scope.model.submit($scope.model); + } + } + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + eventsService.emit("dialogs.memberGroupPicker.select", args); + + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + select(args.node.name, args.node.id); + + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + + activate(); + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/membergrouppicker/membergrouppicker.html new file mode 100644 index 0000000000..22fdeaf8be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/membergrouppicker/membergrouppicker.html @@ -0,0 +1,12 @@ +
    + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html new file mode 100644 index 0000000000..e4a8d36263 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html @@ -0,0 +1,34 @@ +
    + +
    + + +
    + + + + +
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/move/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/move/move.controller.js new file mode 100644 index 0000000000..a7a14354ed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/move/move.controller.js @@ -0,0 +1,118 @@ + (function() { + "use strict"; + + function MoveOverlay($scope, localizationService, eventsService) { + + var vm = this; + + vm.hideSearch = hideSearch; + vm.selectResult = selectResult; + vm.onSearchResults = onSearchResults; + + var dialogOptions = $scope.model; + var searchText = "Search..."; + var node = dialogOptions.currentNode; + + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("actions_move"); + } + + $scope.model.relateToOriginal = true; + $scope.dialogTreeEventHandler = $({}); + + vm.searchInfo = { + searchFromId: null, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + }; + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + vm.searchInfo.showSearch = true; + vm.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + vm.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + } + else { + //eventsService.emit("editors.content.copyController.select", args); + + if ($scope.model.target) { + //un-select if there's a current one selected + $scope.model.target.selected = false; + } + + $scope.model.target = args.node; + $scope.model.target.selected = true; + } + + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + //check if any of the items are list views, if so we need to add a custom + // child: A node to activate the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon umb-tree-icon sprTree icon-search", + cssClasses: ["not-published"] + } + ]; + } + }); + } + } + + function hideSearch() { + vm.searchInfo.showSearch = false; + vm.searchInfo.searchFromId = null; + vm.searchInfo.searchFromName = null; + vm.searchInfo.results = []; + } + + // method to select a search result + function selectResult(evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + } + + //callback when there are search results + function onSearchResults(results) { + vm.searchInfo.results = results; + vm.searchInfo.showSearch = true; + } + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + }); + + + } + + angular.module("umbraco").controller("Umbraco.Overlays.MoveOverlay", MoveOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/move/move.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/move/move.html new file mode 100644 index 0000000000..c30f911cd9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/move/move.html @@ -0,0 +1,33 @@ +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js new file mode 100644 index 0000000000..e74bfd20a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -0,0 +1,498 @@ +//used for the media picker dialog +angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", + function ($scope, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService) { + + var tree = null; + var dialogOptions = $scope.model; + $scope.dialogTreeEventHandler = $({}); + $scope.section = dialogOptions.section; + $scope.treeAlias = dialogOptions.treeAlias; + $scope.multiPicker = dialogOptions.multiPicker; + $scope.hideHeader = true; + $scope.searchInfo = { + searchFromId: dialogOptions.startNodeId, + searchFromName: null, + showSearch: false, + results: [], + selectedSearchResults: [] + } + + $scope.model.selection = []; + + $scope.init = function(contentType) { + + if(contentType === "content") { + entityType = "Document"; + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); + } + } else if(contentType === "member") { + entityType = "Member"; + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); + } + } else if(contentType === "media") { + entityType = "Media"; + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); + } + } + } + + //create the custom query string param for this tree + $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; + $scope.customTreeParams += dialogOptions.customTreeParams ? "&" + dialogOptions.customTreeParams : ""; + + var searchText = "Search..."; + localizationService.localize("general_search").then(function (value) { + searchText = value + "..."; + }); + + // Allow the entity type to be passed in but defaults to Document for backwards compatibility. + var entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; + + + //min / max values + if (dialogOptions.minNumber) { + dialogOptions.minNumber = parseInt(dialogOptions.minNumber, 10); + } + if (dialogOptions.maxNumber) { + dialogOptions.maxNumber = parseInt(dialogOptions.maxNumber, 10); + } + + if (dialogOptions.section === "member") { + entityType = "Member"; + } + else if (dialogOptions.section === "media") { + entityType = "Media"; + } + + //Configures filtering + if (dialogOptions.filter) { + + dialogOptions.filterExclude = false; + dialogOptions.filterAdvanced = false; + + //used advanced filtering + if (angular.isFunction(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } + else if (angular.isObject(dialogOptions.filter)) { + dialogOptions.filterAdvanced = true; + } + else { + if (dialogOptions.filter.startsWith("!")) { + dialogOptions.filterExclude = true; + dialogOptions.filter = dialogOptions.filter.substring(1); + } + + //used advanced filtering + if (dialogOptions.filter.startsWith("{")) { + dialogOptions.filterAdvanced = true; + //convert to object + dialogOptions.filter = angular.fromJson(dialogOptions.filter); + } + } + } + + function nodeExpandedHandler(ev, args) { + if (angular.isArray(args.children)) { + + //iterate children + _.each(args.children, function (child) { + + //check if any of the items are list views, if so we need to add some custom + // children: A node to activate the search, any nodes that have already been + // selected in the search + if (child.metaData.isContainer) { + child.hasChildren = true; + child.children = [ + { + level: child.level + 1, + hasChildren: false, + parent: function () { + return child; + }, + name: searchText, + metaData: { + listViewNode: child, + }, + cssClass: "icon-search", + cssClasses: ["not-published"] + } + ]; + //add base transition classes to this node + child.cssClasses.push("tree-node-slide-up"); + + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function(i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function(item) { + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: "icon umb-tree-icon sprTree " + item.icon, + level: child.level + 1, + metaData: { + isSearchResult: true + }, + hasChildren: false, + parent: function () { + return child; + } + }); + }); + } + + //now we need to look in the already selected search results and + // toggle the check boxes for those ones that are listed + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + if (exists) { + child.selected = true; + } + }); + + //check filter + performFiltering(args.children); + } + } + + //gets the tree object when it loads + function treeLoadedHandler(ev, args) { + tree = args.tree; + } + + //wires up selection + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if (args.node.metaData.listViewNode) { + //check if list view 'search' node was selected + + $scope.searchInfo.showSearch = true; + $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; + $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; + + //add transition classes + var listViewNode = args.node.parent(); + listViewNode.cssClasses.push('tree-node-slide-up-hide-active'); + } + else if (args.node.metaData.isSearchResult) { + //check if the item selected was a search result from a list view + + //unselect + select(args.node.name, args.node.id); + + //remove it from the list view children + var listView = args.node.parent(); + listView.children = _.reject(listView.children, function(child) { + return child.id == args.node.id; + }); + + //remove it from the custom tracked search result list + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function (i) { + return i.id == args.node.id; + }); + } + else { + eventsService.emit("dialogs.treePickerController.select", args); + + if (args.node.filtered) { + return; + } + + //This is a tree node, so we don't have an entity to pass in, it will need to be looked up + //from the server in this method. + select(args.node.name, args.node.id); + + //toggle checked state + args.node.selected = args.node.selected === true ? false : true; + } + } + + /** Method used for selecting a node */ + function select(text, id, entity) { + //if we get the root, we just return a constructed entity, no need for server data + if (id < 0) { + if ($scope.multiPicker) { + + if (entity) { + multiSelectItem(entity); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, entityType).then(function (ent) { + multiSelectItem(ent); + }); + } + + } + else { + var node = { + alias: null, + icon: "icon-folder", + id: id, + name: text + }; + $scope.model.selection.push(node); + $scope.model.submit($scope.model); + } + } + else { + + if ($scope.multiPicker) { + + if (entity) { + multiSelectItem(entity); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, entityType).then(function (ent) { + multiSelectItem(ent); + }); + } + + } + + else { + + $scope.hideSearch(); + + //if an entity has been passed in, use it + if (entity) { + $scope.model.selection.push(entity); + $scope.model.submit($scope.model); + } else { + //otherwise we have to get it from the server + entityResource.getById(id, entityType).then(function (ent) { + $scope.model.selection.push(ent); + $scope.model.submit($scope.model); + }); + } + } + } + } + + function multiSelectItem(item) { + + var found = false; + var foundIndex = 0; + + if($scope.model.selection.length > 0) { + for(i = 0; $scope.model.selection.length > i; i++) { + var selectedItem = $scope.model.selection[i]; + if(selectedItem.id === item.id) { + found = true; + foundIndex = i; + } + } + } + + if(found) { + $scope.model.selection.splice(foundIndex, 1); + } else { + $scope.model.selection.push(item); + } + + } + + function performFiltering(nodes) { + + if (!dialogOptions.filter) { + return; + } + + //remove any list view search nodes from being filtered since these are special nodes that always must + // be allowed to be clicked on + nodes = _.filter(nodes, function(n) { + return !angular.isObject(n.metaData.listViewNode); + }); + + if (dialogOptions.filterAdvanced) { + + //filter either based on a method or an object + var filtered = angular.isFunction(dialogOptions.filter) + ? _.filter(nodes, dialogOptions.filter) + : _.where(nodes, dialogOptions.filter); + + angular.forEach(filtered, function (value, key) { + value.filtered = true; + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + }); + } else { + var a = dialogOptions.filter.toLowerCase().split(','); + angular.forEach(nodes, function (value, key) { + + var found = a.indexOf(value.metaData.contentType.toLowerCase()) >= 0; + + if (!dialogOptions.filterExclude && !found || dialogOptions.filterExclude && found) { + value.filtered = true; + + if (dialogOptions.filterCssClass) { + if (!value.cssClasses) { + value.cssClasses = []; + } + value.cssClasses.push(dialogOptions.filterCssClass); + } + } + }); + } + } + + $scope.multiSubmit = function (result) { + entityResource.getByIds(result, entityType).then(function (ents) { + $scope.submit(ents); + }); + }; + + /** method to select a search result */ + $scope.selectResult = function (evt, result) { + + if (result.filtered) { + return; + } + + result.selected = result.selected === true ? false : true; + + //since result = an entity, we'll pass it in so we don't have to go back to the server + select(result.name, result.id, result); + + //add/remove to our custom tracked list of selected search results + if (result.selected) { + $scope.searchInfo.selectedSearchResults.push(result); + } + else { + $scope.searchInfo.selectedSearchResults = _.reject($scope.searchInfo.selectedSearchResults, function(i) { + return i.id == result.id; + }); + } + + //ensure the tree node in the tree is checked/unchecked if it already exists there + if (tree) { + var found = treeService.getDescendantNode(tree.root, result.id); + if (found) { + found.selected = result.selected; + } + } + + }; + + $scope.hideSearch = function () { + + //Traverse the entire displayed tree and update each node to sync with the selected search results + if (tree) { + + //we need to ensure that any currently displayed nodes that get selected + // from the search get updated to have a check box! + function checkChildren(children) { + _.each(children, function (child) { + //check if the id is in the selection, if so ensure it's flagged as selected + var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { + return child.id == selected.id; + }); + //if the curr node exists in selected search results, ensure it's checked + if (exists) { + child.selected = true; + } + //if the curr node does not exist in the selected search result, and the curr node is a child of a list view search result + else if (child.metaData.isSearchResult) { + //if this tree node is under a list view it means that the node was added + // to the tree dynamically under the list view that was searched, so we actually want to remove + // it all together from the tree + var listView = child.parent(); + listView.children = _.reject(listView.children, function(c) { + return c.id == child.id; + }); + } + + //check if the current node is a list view and if so, check if there's any new results + // that need to be added as child nodes to it based on search results selected + if (child.metaData.isContainer) { + + child.cssClasses = _.reject(child.cssClasses, function(c) { + return c === 'tree-node-slide-up-hide-active'; + }); + + var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function (i) { + return i.parentId == child.id; + }); + _.each(listViewResults, function (item) { + var childExists = _.find(child.children, function(c) { + return c.id == item.id; + }); + if (!childExists) { + var parent = child; + child.children.unshift({ + id: item.id, + name: item.name, + cssClass: "icon umb-tree-icon sprTree " + item.icon, + level: child.level + 1, + metaData: { + isSearchResult: true + }, + hasChildren: false, + parent: function () { + return parent; + } + }); + } + }); + } + + //recurse + if (child.children && child.children.length > 0) { + checkChildren(child.children); + } + }); + } + checkChildren(tree.root.children); + } + + + $scope.searchInfo.showSearch = false; + $scope.searchInfo.searchFromId = dialogOptions.startNodeId; + $scope.searchInfo.searchFromName = null; + $scope.searchInfo.results = []; + } + + $scope.onSearchResults = function(results) { + + //filter all items - this will mark an item as filtered + performFiltering(results); + + //now actually remove all filtered items so they are not even displayed + results = _.filter(results, function(item) { + return !item.filtered; + }); + + $scope.searchInfo.results = results; + + //sync with the curr selected results + _.each($scope.searchInfo.results, function (result) { + var exists = _.find($scope.model.selection, function (selectedId) { + return result.id == selectedId; + }); + if (exists) { + result.selected = true; + } + }); + + $scope.searchInfo.showSearch = true; + }; + + $scope.dialogTreeEventHandler.bind("treeLoaded", treeLoadedHandler); + $scope.dialogTreeEventHandler.bind("treeNodeExpanded", nodeExpandedHandler); + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeLoaded", treeLoadedHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html new file mode 100644 index 0000000000..4e9f503013 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html @@ -0,0 +1,34 @@ +
    + +
    + + +
    + + + + +
    + + +
    + +
    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 new file mode 100644 index 0000000000..2ba14f13b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -0,0 +1,176 @@ +angular.module("umbraco") + .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, 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; + $scope.showPasswordFields = false; + $scope.changePasswordButtonState = "init"; + $scope.model.subtitle = "Umbraco version" + " " + $scope.version; + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("general_user"); + } + + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on("historyService.add", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.remove", function (e, args) { + $scope.history = args.all; + })); + evts.push(eventsService.on("historyService.removeAll", function (e, args) { + $scope.history = []; + })); + + $scope.logout = function () { + + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + $scope.model.close(); + }); + + + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + $scope.model.close(); + $location.path("/logout"); + }; + + $scope.gotoHistory = function (link) { + $location.path(link); + $scope.model.close(); + }; + + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if ($scope.remainingAuthSeconds > 0) { + $scope.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); + } + + }, 1000, false); // 1 second, do NOT execute a global digest + } + + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.model.title = user.name; + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + } + + updateUserInfo(); + + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + + }); + + /* ---------- UPDATE PASSWORD ---------- */ + + //create the initial model for change password property editor + $scope.changePasswordModel = { + alias: "_umb_password", + view: "changepassword", + config: {}, + value: {} + }; + + //go get the config for the membership provider and add it to the model + currentUserResource.getMembershipProviderConfig().then(function(data) { + $scope.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + $scope.changePasswordModel.config.hasPassword = true; + $scope.changePasswordModel.config.disableToggle = true; + $scope.changePasswordModel.config.enableReset = false; + }); + + $scope.changePassword = function() { + + if (formHelper.submitForm({ scope: $scope })) { + + $scope.changePasswordButtonState = "busy"; + + currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { + + //if the password has been reset, then update our model + if (data.value) { + $scope.changePasswordModel.value.generatedPassword = data.value; + } + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + $scope.changePasswordButtonState = "success"; + $timeout(function() { + $scope.togglePasswordFields(); + }, 2000); + + }, function (err) { + + formHelper.handleError(err); + + $scope.changePasswordButtonState = "error"; + + }); + + } + + }; + + $scope.togglePasswordFields = function() { + clearPasswordFields(); + $scope.showPasswordFields = !$scope.showPasswordFields; + } + + function clearPasswordFields() { + $scope.changePasswordModel.value.newPassword = ""; + $scope.changePasswordModel.confirm = ""; + } + + }); 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 new file mode 100644 index 0000000000..9a04e9117e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -0,0 +1,123 @@ +
    + +
    + +
    + +

    + + : {{remainingAuthSeconds | timespan}} + +

    + + + + + + + + + + +
    + +
    + +
    + External login providers +
    + +
    + +
    + + +
    + + +
    + +
    + +
    +
    + +
    + +
    + +
    + Change password +
    + +
    + + + + + + + + + +
    + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.controller.js new file mode 100644 index 0000000000..5d42edd3a5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.controller.js @@ -0,0 +1,25 @@ +angular.module("umbraco") + .controller("Umbraco.Overlays.YsodController", function ($scope, legacyResource, treeService, navigationService, localizationService) { + + if(!$scope.model.title) { + $scope.model.title = localizationService.localize("errors_receivedErrorFromServer"); + } + + if ($scope.model.error && $scope.model.error.data && $scope.model.error.data.StackTrace) { + //trim whitespace + $scope.model.error.data.StackTrace = $scope.model.error.data.StackTrace.trim(); + } + + if ($scope.model.error && $scope.model.error.data) { + $scope.model.error.data.InnerExceptions = []; + var ex = $scope.model.error.data.InnerException; + while (ex) { + if (ex.StackTrace) { + ex.StackTrace = ex.StackTrace.trim(); + } + $scope.model.error.data.InnerExceptions.push(ex); + ex = ex.InnerException; + } + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.html new file mode 100644 index 0000000000..9a4ff7cb81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/ysod/ysod.html @@ -0,0 +1,27 @@ +
    + +

    {{model.error.errorMsg}}

    +

    {{model.error.data.ExceptionMessage || model.error.data.Message}}

    + +
    +
    + Exception Details: +
    + {{model.error.data.ExceptionType}}: {{model.error.data.ExceptionMessage}} +
    + +
    +
    + Stacktrace: +
    +
    {{model.error.data.StackTrace}}
    +
    + +
    +
    + Inner Exception: +
    +
    {{e.ExceptionType}}: {{e.ExceptionMessage}}
    +
    {{e.StackTrace}}
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-contextmenu.html rename to src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html similarity index 77% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html rename to src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index c88dda456c..db8fd92008 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -1,6 +1,6 @@ -
    - + @@ -8,11 +8,11 @@
- +
-
- - \ No newline at end of file + + diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-sections.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html similarity index 73% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-sections.html rename to src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html index 94d235ee3a..05eb30c242 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-sections.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html @@ -1,47 +1,62 @@ -
\ 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 new file mode 100644 index 0000000000..46e1adbde2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html @@ -0,0 +1,26 @@ +
+ + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html new file mode 100644 index 0000000000..fe13267be0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -0,0 +1,35 @@ +
+ +
+ +
+ +
+ +
+ + + + + {{label}} + {{label}} + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-content-left.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-content-left.html new file mode 100644 index 0000000000..9c5297908b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-content-left.html @@ -0,0 +1 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-content-right.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-content-right.html new file mode 100644 index 0000000000..982ea1868a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-content-right.html @@ -0,0 +1 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-section.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-section.html new file mode 100644 index 0000000000..1b2ab2e639 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header-section.html @@ -0,0 +1 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html new file mode 100644 index 0000000000..5a8e38c47c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/subheader/umb-editor-sub-header.html @@ -0,0 +1,6 @@ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html new file mode 100644 index 0000000000..a63880819b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html @@ -0,0 +1,10 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html new file mode 100644 index 0000000000..44ced2e100 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html @@ -0,0 +1,6 @@ +
+ +
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer-content-left.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer-content-left.html new file mode 100644 index 0000000000..3bab2ea3de --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer-content-left.html @@ -0,0 +1 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer-content-right.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer-content-right.html new file mode 100644 index 0000000000..9d58a97506 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer-content-right.html @@ -0,0 +1 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer.html new file mode 100644 index 0000000000..3052636851 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-footer.html @@ -0,0 +1,7 @@ +
+ +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html new file mode 100644 index 0000000000..fd112a018a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -0,0 +1,69 @@ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ + + + + +
{{ name }}
+ + + + + +
+ +
+ + + + +
+ + + +
+ + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html new file mode 100644 index 0000000000..dbb145e533 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -0,0 +1,22 @@ +
+ + + + Actions + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html new file mode 100644 index 0000000000..ec3260b646 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation.html @@ -0,0 +1,15 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html new file mode 100644 index 0000000000..6327e9f310 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html @@ -0,0 +1,10 @@ +
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html new file mode 100644 index 0000000000..f99603afa4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-view.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/html/umb-control-group.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/html/umb-control-group.html rename to src/Umbraco.Web.UI.Client/src/views/components/html/umb-control-group.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/html/umb-pane.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-pane.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/html/umb-pane.html rename to src/Umbraco.Web.UI.Client/src/views/components/html/umb-pane.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/html/umb-panel.html b/src/Umbraco.Web.UI.Client/src/views/components/html/umb-panel.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/html/umb-panel.html rename to src/Umbraco.Web.UI.Client/src/views/components/html/umb-panel.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-crop.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-crop.html rename to src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-gravity.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html similarity index 62% rename from src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-gravity.html rename to src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html index c878bf8189..21500e7c84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-gravity.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html @@ -1,12 +1,12 @@
-
+
- +
- +
- +
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-thumbnail.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html similarity index 60% rename from src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-thumbnail.html rename to src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html index 424f00cc9a..2f3861ae6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-thumbnail.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-thumbnail.html @@ -1,4 +1,5 @@ -
+
{{}} -
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html new file mode 100644 index 0000000000..9962e2dd84 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/notifications/umb-notifications.html @@ -0,0 +1,21 @@ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay-backdrop.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay-backdrop.html new file mode 100644 index 0000000000..23ffbf643f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay-backdrop.html @@ -0,0 +1 @@ +
{{ numberOfOverlays }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html new file mode 100644 index 0000000000..472408468b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -0,0 +1,76 @@ +
+ + +
+

{{model.title}}

+

{{model.subtitle}}

+
+ +
+
+
+ +
+ +
+ +
{{ model.itemDetails.title }}
+
+ +
{{ model.itemDetails.description }}
+ +
+ +
+ +
+ +
{{ model.confirmSubmit.title }}
+

{{ model.confirmSubmit.description }}

+ + + +
+ + + + + +
+
+ +
+ + + + + +
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html new file mode 100644 index 0000000000..bbf70f7a8e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-editor.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-group.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property-group.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html similarity index 79% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html rename to src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 16432bc1c4..7517795136 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -1,18 +1,19 @@ -
- -
- - - -
- - -
-
-
-
-
-
+
+ +
+ + + +
+ + + +
+
+
+
+
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-tab.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tab.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-tab.html rename to src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tab.html diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-content.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-content.html new file mode 100644 index 0000000000..8e1c7d4d67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-content.html @@ -0,0 +1 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html new file mode 100644 index 0000000000..8522bc3b6a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html @@ -0,0 +1,5 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-tree-search-box.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-tree-search-box.html rename to src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-box.html diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html new file mode 100644 index 0000000000..97f08fe359 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html @@ -0,0 +1,23 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html new file mode 100644 index 0000000000..686937e8c9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-child-selector.html @@ -0,0 +1,37 @@ +
+ +
+
+
+ +
+ + {{ parentName }} + + + () + +
+
+ +
+ +
+
+
+ +
+ {{ selectedChild.name }} +
+
+ +
+
+ + +
Add Child
+
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html new file mode 100644 index 0000000000..c34c3f9283 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html @@ -0,0 +1,16 @@ +
+ + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-confirm.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-confirm.html rename to src/Umbraco.Web.UI.Client/src/views/components/umb-confirm.html diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html new file mode 100644 index 0000000000..499594253e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-content-grid.html @@ -0,0 +1,36 @@ +
+ +
+ + + +
+ +
+ +
+ +
{{ item.name }}
+ +
    +
  • +
    {{ property.header }}:
    +
    {{ item[property.alias] }}
    +
  • +
+ +
+ +
+ + + There are no items to show + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-empty-state.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-empty-state.html new file mode 100644 index 0000000000..e98227753e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-empty-state.html @@ -0,0 +1,9 @@ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html new file mode 100644 index 0000000000..df5eafbbca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-folder-grid.html @@ -0,0 +1,18 @@ +
+ +
+ +
+ +
{{ folder.name }}
+
+ + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-generate-alias.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-generate-alias.html new file mode 100644 index 0000000000..69327474d6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-generate-alias.html @@ -0,0 +1,12 @@ +
+ {{ alias }} +
+ + +
+
+ \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html new file mode 100644 index 0000000000..0fa236fd7f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-grid-selector.html @@ -0,0 +1,43 @@ +
+ +
+ +
+
+ +
{{ defaultItem.name }}
+ (Default {{itemLabel}}) +
+ +
+ +
+
+ +
{{ selectedItem.name }}
+ Set as default +
+ +
+ + +
+
Choose extra {{ itemLabel }}
+
Choose default {{ itemLabel }}
+
+
+ +
+ +
+ Akk {{itemLabel}}s are added +
+ + + + +
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 new file mode 100644 index 0000000000..3533255288 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -0,0 +1,299 @@ +
+ + + + + + + + + + + + + + + +
+ +
+ +
    + +
  • + + + + +
    +
    +
    + + + + +
    + + +
    + +
    + + + +
    + +
    + + +
    + + + +
    +
    + +
    +
    + + +
    + +
    +
    +
    +
    + +
    + + {{ tab.inheritedFromName }} + + + {{ contentTypeName }} + , + +
    + +
    + +
      + +
    • + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
      + +
      + +
      + +
      + +
      + +
      + + +
      + + +
      + +
      {{ property.alias }}
      + + + +
      + + +
      +
      +
      + +
      + +
      +
      +
      + +
      + + {{ property.label }} + ({{ property.alias }}) +
      + +
      + + +
      + +
      + + + {{property.dataTypeName}} + + + +
      + * + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + + {{property.contentTypeName}} +
      + +
      + + +
      + +
      + + + + + + +
      + + +
      + +
      + + +
      + +
      + + +
      + + + +
      + +
      + +
      + +
      + +
    • + +
    + +
    + +
    + +
  • + +
+ + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html new file mode 100644 index 0000000000..6368bb1e37 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-keyboard-shortcuts-overview.html @@ -0,0 +1,60 @@ +
+ +
+
show shortcuts
+
+
+
alt
+
+
+
+
+
shift
+
+
+
+
+
k
+
+
+
+ +
+ + + + + +
+ +
+ +
{{ keyboardShortcutGroup.name }}
+ +
+ +
+
{{ keyboardShortcut.description }}
+
+ +
+ +
+
{{ key.key }}
+
+ + + - +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html new file mode 100644 index 0000000000..c7dba13bae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html @@ -0,0 +1,14 @@ +
+ +
+ +
+ +
+
+ +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-layout.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-layout.html new file mode 100644 index 0000000000..a4ea2d2481 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-layout.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html new file mode 100644 index 0000000000..5afd3012eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-list-view-settings.html @@ -0,0 +1,39 @@ +
+ +
+
+ + +
+
+ +
+ +
+
+
{{ dataType.name }} (default)
+ +
+ Create custom list view + Remove custom list view +
+
+ +
+ + +
+ + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-load-indicator.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-load-indicator.html new file mode 100644 index 0000000000..4d545e21e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-load-indicator.html @@ -0,0 +1,5 @@ +
    +
  • +
  • +
  • +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html new file mode 100644 index 0000000000..217d420a3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html @@ -0,0 +1,35 @@ + + +
+ + + + + + + + + + + +
+ +
Required alias
+
Invalid alias
+
+ +
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 new file mode 100644 index 0000000000..5cd6d5c82f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -0,0 +1,20 @@ +
+
+
+ + + +
+ +
{{item.name}}
+
+ +
+ {{item.name}} + {{item.name}} + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html new file mode 100644 index 0000000000..9e4a1697a3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html @@ -0,0 +1,26 @@ + 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 new file mode 100644 index 0000000000..fb5e992d81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html @@ -0,0 +1,60 @@ +
+
+ +
+
+
+ +
+ + +
+
+ +
+
+ +
+ + +
+
+ + +
+
+ {{item[column.alias]}} +
+
+
+
+ + + There are no items show in the list. + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-tooltip.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-tooltip.html new file mode 100644 index 0000000000..18095e599c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-tooltip.html @@ -0,0 +1 @@ +
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 new file mode 100644 index 0000000000..60cec9996f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -0,0 +1,101 @@ +
+ + + + +
+ + +
+ + + + + + +
+ - or click here to choose files +
+
+
+ + +
    + + +
  • + + +
    {{ file.name }}
    + + +
    + +
    + +
  • + +
  • + + +
    {{ currentFile.name }}
    + + +
    + +
    +
  • + + +
  • + + +
    {{ queued.name }}
    +
  • + +
  • + + +
    + + {{ file.name }} + + + + "{{maxFileSize}}" + + + + {{file.serverErrorMessage}} + + +
    + + +
    + +
    + +
  • +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index fc3015ed09..9fb6e60c66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService) { + function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -7,7 +7,8 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", searchText = value + "..."; }); - $scope.relateToOriginal = false; + $scope.relateToOriginal = true; + $scope.recursive = true; $scope.dialogTreeEventHandler = $({}); $scope.busy = false; $scope.searchInfo = { @@ -42,7 +43,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.target = args.node; $scope.target.selected = true; } - + } function nodeExpandedHandler(ev, args) { @@ -50,7 +51,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", //iterate children _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add a custom + //check if any of the items are list views, if so we need to add a custom // child: A node to activate the search if (child.metaData.isContainer) { child.hasChildren = true; @@ -78,24 +79,24 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.searchInfo.results = []; } - // method to select a search result + // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; nodeSelectHandler(evt, { event: evt, node: result }); }; - //callback when there are search results + //callback when there are search results $scope.onSearchResults = function (results) { $scope.searchInfo.results = results; $scope.searchInfo.showSearch = true; }; - + $scope.copy = function () { $scope.busy = true; $scope.error = false; - contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal }) + contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) .then(function (path) { $scope.error = false; $scope.success = true; @@ -119,6 +120,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.success = false; $scope.error = err; $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } }); }; @@ -129,4 +136,4 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); }); - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index 03de8af21e..e8e729fd72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -10,11 +10,17 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ //setup scope vars $scope.defaultButton = null; - $scope.subButtons = []; - $scope.currentSection = appState.getSectionState("currentSection"); - $scope.currentNode = null; //the editors affiliated node - $scope.isNew = $routeParams.create; - + $scope.subButtons = []; + + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentNode = null; + $scope.page.menu.currentSection = appState.getSectionState("currentSection"); + $scope.page.listViewPath = null; + $scope.page.isNew = $routeParams.create; + $scope.page.buttonGroupState = "init"; + function init(content) { var buttons = contentEditingHelper.configureContentEditorButtons({ @@ -48,7 +54,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ if (!$scope.content.isChildOfListView) { navigationService.syncTree({ tree: "content", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.currentNode = syncArgs.node; + $scope.page.menu.currentNode = syncArgs.node; }); } else if (initialLoad === true) { @@ -61,7 +67,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ umbRequestHelper.resourcePromise( $http.get(content.treeNodeUrl), 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.currentNode = node; + $scope.page.menu.currentNode = node; }); } } @@ -70,22 +76,30 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ function performSave(args) { var deferred = $q.defer(); + $scope.page.buttonGroupState = "busy"; + contentEditingHelper.contentEditorPerformSave({ statusMessage: args.statusMessage, saveMethod: args.saveMethod, scope: $scope, - content: $scope.content + content: $scope.content, + action: args.action }).then(function (data) { //success init($scope.content); syncTreeNode($scope.content, data.path); + $scope.page.buttonGroupState = "success"; + deferred.resolve(data); }, function (err) { //error if (err) { editorState.set($scope.content); } + + $scope.page.buttonGroupState = "error"; + deferred.reject(err); }); @@ -102,26 +116,35 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ } if ($routeParams.create) { + + $scope.page.loading = true; + //we are creating so get an empty content item contentResource.getScaffold($routeParams.id, $routeParams.doctype) .then(function (data) { - $scope.loaded = true; + $scope.content = data; init($scope.content); resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + }); } else { + + $scope.page.loading = true; + //we are editing so get the content item from the server contentResource.getById($routeParams.id) .then(function (data) { - $scope.loaded = true; + $scope.content = data; if (data.isChildOfListView && data.trashed === false) { - $scope.listViewPath = ($routeParams.page) + $scope.page.listViewPath = ($routeParams.page) ? "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : "/content/content/edit/" + data.parentId; } @@ -137,6 +160,9 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ syncTreeNode($scope.content, data.path, true); resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + }); } @@ -145,6 +171,8 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { + $scope.page.buttonGroupState = "busy"; + contentResource.unPublish($scope.content.id) .then(function (data) { @@ -160,21 +188,23 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ syncTreeNode($scope.content, data.path); + $scope.page.buttonGroupState = "success"; + }); } }; $scope.sendToPublish = function () { - return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending..." }); + return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); }; $scope.saveAndPublish = function () { - return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." }); + return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); }; $scope.save = function () { - return performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." }); + return performSave({ saveMethod: contentResource.save, statusMessage: "Saving...", action: "save" }); }; $scope.preview = function (content) { @@ -194,19 +224,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ } - - }; - // this method is called for all action buttons and then we proxy based on the btn definition - $scope.performAction = function (btn) { - - if (!btn || !angular.isFunction(btn.handler)) { - throw "btn.handler must be a function reference"; - } - - if (!$scope.busy) { - btn.handler.apply(this); - } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index 5f65199269..75969730a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService) { + function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -120,6 +120,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", $scope.success = false; $scope.error = err; $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.recyclebin.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.recyclebin.controller.js index f72075149c..4a8c50db39 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.recyclebin.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.recyclebin.controller.js @@ -8,24 +8,46 @@ * */ -function ContentRecycleBinController($scope, $routeParams, dataTypeResource, navigationService) { +function ContentRecycleBinController($scope, $routeParams, contentResource, navigationService, localizationService) { + + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.page = {}; + $scope.page.name = "Recycle Bin"; + $scope.page.nameLocked = true; //ensures the list view doesn't actually load until we query for the list view config // for the section $scope.listViewPath = null; $routeParams.id = "-20"; - dataTypeResource.getById(-95).then(function (result) { - _.each(result.preValues, function (i) { - $scope.model.config[i.key] = i.value; + contentResource.getRecycleBin().then(function (result) { + //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a + // single property, so we'll extract that property (list view) and use it's data. + var listproperty = result.tabs[0].properties[0]; + + _.each(listproperty.config, function (val, key) { + $scope.model.config[key] = val; }); $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; }); - $scope.model = { config: { entityType: $routeParams.section } }; + $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; // sync tree node navigationService.syncTree({ tree: "content", path: ["-1", $routeParams.id], forceReload: false }); + + localizePageName(); + + function localizePageName() { + + var pageName = "general_recycleBin"; + + localizationService.localize(pageName).then(function (value) { + $scope.page.name = value; + }); + + } } angular.module('umbraco').controller("Umbraco.Editors.Content.RecycleBinController", ContentRecycleBinController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index fddc07db22..3f2bfcdd3b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -9,9 +9,9 @@
-
-

{{error.errorMsg}}

-

{{error.data.Message}}

+
+
{{error.errorMsg}}
+

{{error.data.message}}

@@ -56,6 +56,12 @@ + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index d3bc28bc55..c40fb39351 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -1,9 +1,9 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-item-sorter.html b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-item-sorter.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-item-sorter.html rename to src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-item-sorter.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-login.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-login.html rename to src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-login.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-optionsmenu.html b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-optionsmenu.html similarity index 82% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-optionsmenu.html rename to src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-optionsmenu.html index a46421b513..f834dfb93b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-optionsmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-optionsmenu.html @@ -1,5 +1,5 @@ -
- +
+ Actions @@ -7,7 +7,7 @@ -
diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/html/umb-photo-folder.html b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-photo-folder.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/html/umb-photo-folder.html rename to src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-photo-folder.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-tab-view.html b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-tab-view.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/umb-tab-view.html rename to src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-tab-view.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/html/umb-upload-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-upload-dropzone.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/directives/html/umb-upload-dropzone.html rename to src/Umbraco.Web.UI.Client/src/views/directives/_obsolete/umb-upload-dropzone.html diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/button-group.html b/src/Umbraco.Web.UI.Client/src/views/directives/button-group.html deleted file mode 100644 index 83ee647361..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/directives/button-group.html +++ /dev/null @@ -1,21 +0,0 @@ - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-folder.html b/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-folder.html deleted file mode 100644 index e580b4c956..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/directives/imaging/umb-image-folder.html +++ /dev/null @@ -1,34 +0,0 @@ -
- -
- -
- -

Click to upload

- -
- - - -
- - - -
-
-
- -
- - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-avatar.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-avatar.html deleted file mode 100644 index b89bd2065a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-avatar.html +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-editor.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-editor.html deleted file mode 100644 index cb7f084650..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-editor.html +++ /dev/null @@ -1,3 +0,0 @@ -
- -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-notifications.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-notifications.html deleted file mode 100644 index cd611d85e6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-notifications.html +++ /dev/null @@ -1,18 +0,0 @@ -
- -
diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-tree-search-results.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-tree-search-results.html deleted file mode 100644 index 2dc864f00c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-tree-search-results.html +++ /dev/null @@ -1,17 +0,0 @@ - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js new file mode 100644 index 0000000000..c666a2159c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js @@ -0,0 +1,63 @@ +angular.module("umbraco") +.controller("Umbraco.Editors.DocumentTypes.CopyController", + function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + contentTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html new file mode 100644 index 0000000000..db1a0db640 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -0,0 +1,51 @@ +
+ +
+
+ +

+ Select the folder to copy {{currentNode.name}} to in the tree structure below +

+ +
+
+
+ +
+
{{error.errorMsg}}
+

{{error.data.message}}

+
+ +
+
+ {{currentNode.name}} was copied underneath {{target.name}}
+ +
+ +
+ +
+ + +
+ +
+
+
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js new file mode 100644 index 0000000000..4e734b76a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js @@ -0,0 +1,76 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.CreateController + * @function + * + * @description + * The controller for the doc type creation dialog + */ +function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { + + $scope.model = { + allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === "container", + folderName: "", + creatingFolder: false, + }; + + var node = $scope.dialogOptions.currentNode, + localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); + + $scope.showCreateFolder = function() { + $scope.model.creatingFolder = true; + }; + + $scope.createContainer = function() { + + if (formHelper.submitForm({scope: $scope, formCtrl: this.createFolderForm, statusMessage: localizeCreateFolder})) { + + contentTypeResource.createContainer(node.id, $scope.model.folderName).then(function(folderId) { + + navigationService.hideMenu(); + + var currPath = node.path ? node.path : "-1"; + + navigationService.syncTree({ + tree: "documenttypes", + path: currPath + "," + folderId, + forceReload: true, + activate: true + }); + + formHelper.resetForm({ + scope: $scope + }); + + var section = appState.getSectionState("currentSection"); + + }, function(err) { + + $scope.error = err; + + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + } + }; + + $scope.createDocType = function() { + $location.search('create', null); + $location.search('notemplate', null); + $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + }; + + $scope.createComponent = function() { + $location.search('create', null); + $location.search('notemplate', null); + $location.path("/settings/documenttypes/edit/" + node.id).search("create", "true").search("notemplate", "true"); + navigationService.hideMenu(); + }; +} + +angular.module('umbraco').controller("Umbraco.Editors.DocumentTypes.CreateController", DocumentTypesCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html new file mode 100644 index 0000000000..2a99fb7e3d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html @@ -0,0 +1,56 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js new file mode 100644 index 0000000000..c920e05480 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.controller.js @@ -0,0 +1,50 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.DeleteController + * @function + * + * @description + * The controller for deleting content + */ +function DocumentTypesDeleteController($scope, dataTypeResource, contentTypeResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.performContainerDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + contentTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.DeleteController", DocumentTypesDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html new file mode 100644 index 0000000000..d0826bf80a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/delete.html @@ -0,0 +1,43 @@ +
+ +
+ +

+ Are you sure you want to delete + {{currentNode.name}} ? +

+ + +
+ + +
+ +
+

+ + + All Documents + + + using this document type will be deleted permanently, please confirm you want to delete these as well. + +

+ +
+ + + + + +
+
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js new file mode 100644 index 0000000000..0b304aaef4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -0,0 +1,376 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.EditController + * @function + * + * @description + * The controller for the content type editor + */ +(function () { + "use strict"; + + function DocumentTypesEditController($scope, $routeParams, $injector, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { + + var vm = this; + var localizeSaving = localizationService.localize("general_saving"); + var evts = []; + + vm.save = save; + + vm.currentNode = null; + vm.contentType = {}; + + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.navigation = [ + { + "name": localizationService.localize("general_design"), + "icon": "icon-document-dashed-line", + "view": "views/documenttypes/views/design/design.html", + "active": true + }, + { + "name": localizationService.localize("general_listView"), + "icon": "icon-list", + "view": "views/documenttypes/views/listview/listview.html" + }, + { + "name": localizationService.localize("general_rights"), + "icon": "icon-keychain", + "view": "views/documenttypes/views/permissions/permissions.html" + }, + { + "name": localizationService.localize("treeHeaders_templates"), + "icon": "icon-layout", + "view": "views/documenttypes/views/templates/templates.html" + } + ]; + + vm.page.keyboardShortcutsOverview = [ + { + "name": localizationService.localize("main_sections"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_navigateSections"), + "keys": [{ "key": "1" }, { "key": "4" }], + "keyRange": true + } + ] + }, + { + "name": localizationService.localize("general_design"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_addTab"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + }, + { + "description": localizationService.localize("shortcuts_addProperty"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] + }, + { + "description": localizationService.localize("shortcuts_addEditor"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] + }, + { + "description": localizationService.localize("shortcuts_editDataType"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] + } + ] + }, + { + "name": localizationService.localize("general_listView"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_toggleListView"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] + } + ] + }, + { + "name": localizationService.localize("general_rights"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_toggleAllowAsRoot"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] + }, + { + "description": localizationService.localize("shortcuts_addChildNode"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] + } + ] + }, + { + "name": localizationService.localize("treeHeaders_templates"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_addTemplate"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + } + ] + } + ]; + + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: "ctrl+s", + hotKeyWhenHidden: true, + labelKey: "buttons_save", + letter: "S", + type: "submit", + handler: function () { vm.save(); } + }; + vm.page.subButtons = [{ + hotKey: "ctrl+g", + hotKeyWhenHidden: true, + labelKey: "buttons_saveAndGenerateModels", + letter: "G", + handler: function () { + + vm.page.saveButtonState = "busy"; + + vm.save().then(function (result) { + + vm.page.saveButtonState = "busy"; + + localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { + localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + + contentTypeHelper.generateModels().then(function (result) { + + // generateModels() returns the dashboard content + if (!result.lastError) { + + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function(statusResult) { + vm.page.modelsBuilder = statusResult; + }); + + //clear and add success + vm.page.saveButtonState = "init"; + localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { + notificationsService.success(value); + }); + + } else { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { + notificationsService.error(value); + }); + } + + }, function () { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { + notificationsService.error(value); + }); + }); + + }); + + } + }]; + } + }); + + if ($routeParams.create) { + vm.page.loading = true; + + //we are creating so get an empty data type item + contentTypeResource.getScaffold($routeParams.id) + .then(function (dt) { + + init(dt); + + vm.page.loading = false; + + }); + } + else { + loadDocumentType(); + } + + function loadDocumentType() { + + vm.page.loading = true; + + contentTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); + + syncTreeNode(vm.contentType, dt.path, true); + + vm.page.loading = false; + + }); + + } + + + /* ---------- SAVE ---------- */ + + function save() { + + // only save if there is no overlays open + if(overlayHelper.getNumberOfOverlays() === 0) { + + var deferred = $q.defer(); + + vm.page.saveButtonState = "busy"; + + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: contentTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type 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, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function(group) { + if (!group.name) return; + + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + + var savedGroup = savedContentType.groups[k]; + if (!group.id) group.id = savedGroup.id; + + group.properties.forEach(function (property) { + if (property.id || !property.alias) return; + + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); + + vm.page.saveButtonState = "success"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + vm.page.saveButtonState = "error"; + + deferred.reject(err); + }); + return deferred.promise; + + } + + } + + function init(contentType) { + + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + + }); + } + + // insert template on new doc types + if (!$routeParams.notemplate && contentType.id === 0) { + contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); + contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); + } + + // convert icons for content type + convertLegacyIcons(contentType); + + //set a shared state + editorState.set(contentType); + + vm.contentType = contentType; + } + + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + + function getDataTypeDetails(property) { + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ tree: "documenttypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + + evts.push(eventsService.on("app.refreshEditor", function(name, error) { + loadDocumentType(); + })); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + + } + + angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html new file mode 100644 index 0000000000..39efb9d1c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.html @@ -0,0 +1,69 @@ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js new file mode 100644 index 0000000000..e82385477a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js @@ -0,0 +1,68 @@ +angular.module("umbraco") +.controller("Umbraco.Editors.DocumentTypes.MoveController", + function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + contentTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "documentTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + eventsService.emit('app.refreshEditor'); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html new file mode 100644 index 0000000000..c982abc0c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -0,0 +1,51 @@ +
+ +
+
+ +

+ Select the folder to move {{currentNode.name}} to in the tree structure below +

+ +
+
+
+ +
+
{{error.errorMsg}}
+

{{error.data.message}}

+
+ +
+
+ {{currentNode.name}} was moved underneath {{target.name}}
+ +
+ +
+ +
+ + +
+ +
+
+
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/property.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/property.html new file mode 100644 index 0000000000..ccd632246c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/property.html @@ -0,0 +1,4 @@ +
+ + {{property.description}} +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/propertygroup.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/propertygroup.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/design/design.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/design/design.html new file mode 100644 index 0000000000..cd6b629b42 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/design/design.html @@ -0,0 +1,4 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/listview/listview.html new file mode 100644 index 0000000000..c27d885483 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/listview/listview.html @@ -0,0 +1,19 @@ +
+ +
+
+ +
+ +
+ + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js new file mode 100644 index 0000000000..071f4c6480 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js @@ -0,0 +1,81 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.PropertyController + * @function + * + * @description + * The controller for the content type editor property dialog + */ +(function() { + 'use strict'; + + function PermissionsController($scope, contentTypeResource, iconHelper, contentTypeHelper, localizationService) { + + /* ----------- SCOPE VARIABLES ----------- */ + + var vm = this; + var childNodeSelectorOverlayTitle = ""; + + vm.contentTypes = []; + vm.selectedChildren = []; + + vm.overlayTitle = ""; + + vm.addChild = addChild; + vm.removeChild = removeChild; + + /* ---------- INIT ---------- */ + + init(); + + function init() { + + childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); + + contentTypeResource.getAll().then(function(contentTypes){ + + vm.contentTypes = contentTypes; + + // convert legacy icons + iconHelper.formatContentTypeIcons(vm.contentTypes); + + vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.contentTypes); + + if($scope.model.id === 0) { + contentTypeHelper.insertChildNodePlaceholder(vm.contentTypes, $scope.model.name, $scope.model.icon, $scope.model.id); + } + + }); + + } + + function addChild($event) { + vm.childNodeSelectorOverlay = { + view: "itempicker", + title: childNodeSelectorOverlayTitle, + availableItems: vm.contentTypes, + selectedItems: vm.selectedChildren, + event: $event, + show: true, + submit: function(model) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + vm.childNodeSelectorOverlay.show = false; + vm.childNodeSelectorOverlay = null; + } + }; + } + + function removeChild(selectedChild, index) { + // remove from vm + vm.selectedChildren.splice(index, 1); + + // remove from content type model + var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); + $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.DocumentType.PermissionsController", PermissionsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html new file mode 100644 index 0000000000..d9c7d185da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html @@ -0,0 +1,47 @@ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+ + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js new file mode 100644 index 0000000000..fe1b0f0e7e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.controller.js @@ -0,0 +1,45 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.TemplatesController + * @function + * + * @description + * The controller for the content type editor templates sub view + */ +(function() { + 'use strict'; + + function TemplatesController($scope, entityResource, contentTypeHelper, $routeParams) { + + /* ----------- SCOPE VARIABLES ----------- */ + + var vm = this; + + vm.availableTemplates = []; + vm.updateTemplatePlaceholder = false; + + + /* ---------- INIT ---------- */ + + init(); + + function init() { + + entityResource.getAll("Template").then(function(templates){ + + vm.availableTemplates = templates; + + // update placeholder template information on new doc types + if (!$routeParams.notemplate && $scope.model.id === 0) { + vm.updateTemplatePlaceholder = true; + vm.availableTemplates = contentTypeHelper.insertTemplatePlaceholder(vm.availableTemplates); + } + + }); + + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.DocumentType.TemplatesController", TemplatesController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.html new file mode 100644 index 0000000000..0cfe76cd7a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/templates/templates.html @@ -0,0 +1,23 @@ +
+ +
+
+ +
+ +
+ + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index f131b35e5b..8929d143b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -1,64 +1,75 @@ -
+ + + + - - -
- -
+ -
-
-
- -
+ + - - -
-
-
+ - - + + -
- + + + - - + + -
+ - + -
- -
-
-
+ -
-
+ + - -
-
\ No newline at end of file + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 0895ded13c..fcbb9dd0c7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -12,12 +12,20 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti $scope.currentSection = appState.getSectionState("currentSection"); $scope.currentNode = null; //the editors affiliated node + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentSection = appState.getSectionState("currentSection"); + $scope.page.menu.currentNode = null; //the editors affiliated node + $scope.page.listViewPath = null; + $scope.page.saveButtonState = "init"; + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { if (!$scope.content.isChildOfListView) { navigationService.syncTree({ tree: "media", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - $scope.currentNode = syncArgs.node; + $scope.page.menu.currentNode = syncArgs.node; }); } else if (initialLoad === true) { @@ -30,29 +38,36 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti umbRequestHelper.resourcePromise( $http.get(content.treeNodeUrl), 'Failed to retrieve data for child node ' + content.id).then(function (node) { - $scope.currentNode = node; + $scope.page.menu.currentNode = node; }); } } if ($routeParams.create) { + $scope.page.loading = true; + mediaResource.getScaffold($routeParams.id, $routeParams.doctype) .then(function (data) { - $scope.loaded = true; $scope.content = data; editorState.set($scope.content); + + $scope.page.loading = false; + }); } else { + + $scope.page.loading = true; + mediaResource.getById($routeParams.id) .then(function (data) { - $scope.loaded = true; + $scope.content = data; if (data.isChildOfListView && data.trashed === false) { - $scope.listViewPath = ($routeParams.page) + $scope.page.listViewPath = ($routeParams.page) ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page : "/media/media/edit/" + data.parentId; } @@ -75,7 +90,9 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti }); } - }); + $scope.page.loading = false; + + }); } $scope.save = function () { @@ -83,6 +100,7 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti if (!$scope.busy && formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { $scope.busy = true; + $scope.page.saveButtonState = "busy"; mediaResource.save($scope.content, $routeParams.create, fileManager.getFiles()) .then(function(data) { @@ -100,6 +118,8 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti syncTreeNode($scope.content, data.path); + $scope.page.saveButtonState = "success"; + }, function(err) { contentEditingHelper.handleSaveError({ @@ -117,6 +137,8 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti editorState.set($scope.content); $scope.busy = false; + $scope.page.saveButtonState = "error"; + }); }else{ $scope.busy = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.recyclebin.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.recyclebin.controller.js index 6f4bdc11e2..4e54bd832a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.recyclebin.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.recyclebin.controller.js @@ -8,24 +8,46 @@ * */ -function MediaRecycleBinController($scope, $routeParams, dataTypeResource, navigationService) { +function MediaRecycleBinController($scope, $routeParams, mediaResource, navigationService, localizationService) { + + //ensures the list view doesn't actually load until we query for the list view config + // for the section + $scope.page = {}; + $scope.page.name = "Recycle Bin"; + $scope.page.nameLocked = true; //ensures the list view doesn't actually load until we query for the list view config // for the section $scope.listViewPath = null; $routeParams.id = "-21"; - dataTypeResource.getById(-96).then(function (result) { - _.each(result.preValues, function (i) { - $scope.model.config[i.key] = i.value; + mediaResource.getRecycleBin().then(function (result) { + //we'll get the 'content item' for the recycle bin, we know that it will contain a single tab and a + // single property, so we'll extract that property (list view) and use it's data. + var listproperty = result.tabs[0].properties[0]; + + _.each(listproperty.config, function (val, key) { + $scope.model.config[key] = val; }); $scope.listViewPath = 'views/propertyeditors/listview/listview.html'; }); - $scope.model = { config: { entityType: $routeParams.section } }; + $scope.model = { config: { entityType: $routeParams.section, layouts: [] } }; // sync tree node navigationService.syncTree({ tree: "media", path: ["-1", $routeParams.id], forceReload: false }); + + localizePageName(); + + function localizePageName() { + + var pageName = "general_recycleBin"; + + localizationService.localize(pageName).then(function (value) { + $scope.page.name = value; + }); + + } } angular.module('umbraco').controller("Umbraco.Editors.Media.RecycleBinController", MediaRecycleBinController); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/move.html b/src/Umbraco.Web.UI.Client/src/views/media/move.html index 8cca30770e..95c30dfc40 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/move.html @@ -6,13 +6,13 @@ Choose where to move {{currentNode.name}} to in the tree structure below

-
+

{{error.errorMsg}}

{{error.data.Message}}

-
-

{{currentNode.name}} was moved underneath +

+

{{currentNode.name}} was moved underneath {{target.name}}

@@ -35,4 +35,4 @@ Cancel
-
\ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html b/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html index 0af3a029bf..4a74e4affe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html @@ -1,14 +1,16 @@ - - -
-

Recycle Bin

-
-
- - - -
-
-
-
-
+ +
+ + + + + +
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.controller.js new file mode 100644 index 0000000000..2a1b2463f8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.controller.js @@ -0,0 +1,63 @@ +angular.module("umbraco") +.controller("Umbraco.Editors.MediaTypes.CopyController", + function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.copy = function () { + + $scope.busy = true; + $scope.error = false; + + mediaTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html new file mode 100644 index 0000000000..319e59c4cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html @@ -0,0 +1,51 @@ +
+ +
+
+ +

+ Select the folder to copy {{currentNode.name}} to in the tree structure below +

+ +
+
+
+ +
+
{{error.errorMsg}}
+

{{error.data.message}}

+
+ +
+
+ {{currentNode.name}} was copied underneath {{target.name}}
+ +
+ +
+ +
+ + +
+ +
+
+
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js new file mode 100644 index 0000000000..bc0cf854c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.controller.js @@ -0,0 +1,53 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.CreateController + * @function + * + * @description + * The controller for the media type creation dialog + */ +function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) { + + $scope.model = { + folderName: "", + creatingFolder: false + }; + + var node = $scope.dialogOptions.currentNode, + localizeCreateFolder = localizationService.localize("defaultdialog_createFolder"); + + $scope.showCreateFolder = function() { + $scope.model.creatingFolder = true; + } + + $scope.createContainer = function () { + if (formHelper.submitForm({ + scope: $scope, + formCtrl: this.createFolderForm, + statusMessage: localizeCreateFolder + })) { + mediaTypeResource.createContainer(node.id, $scope.model.folderName).then(function (folderId) { + + navigationService.hideMenu(); + var currPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "mediatypes", path: currPath + "," + folderId, forceReload: true, activate: true }); + + formHelper.resetForm({ scope: $scope }); + + var section = appState.getSectionState("currentSection"); + + }, function(err) { + + //TODO: Handle errors + }); + }; + } + + $scope.createMediaType = function() { + $location.search('create', null); + $location.path("/settings/mediatypes/edit/" + node.id).search("create", "true"); + navigationService.hideMenu(); + } +} + +angular.module('umbraco').controller("Umbraco.Editors.MediaTypes.CreateController", MediaTypesCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html new file mode 100644 index 0000000000..ab83dae80f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/create.html @@ -0,0 +1,47 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.controller.js new file mode 100644 index 0000000000..785550684b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.controller.js @@ -0,0 +1,50 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.DeleteController + * @function + * + * @description + * The controller for the media type delete dialog + */ +function MediaTypesDeleteController($scope, dataTypeResource, mediaTypeResource, treeService, navigationService) { + + $scope.performDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + mediaTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.performContainerDelete = function() { + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + mediaTypeResource.deleteContainerById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + //get the root node before we remove it + var rootNode = treeService.getTreeRoot($scope.currentNode); + + //TODO: Need to sync tree, etc... + treeService.removeNode($scope.currentNode); + navigationService.hideMenu(); + }); + + }; + + $scope.cancel = function() { + navigationService.hideDialog(); + }; +} + +angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.DeleteController", MediaTypesDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html new file mode 100644 index 0000000000..6d33b6ae0c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/delete.html @@ -0,0 +1,39 @@ +
+
+ +

+ Are you sure you want to delete {{currentNode.name}} ? +

+ + +
+ + +
+ +
+

+ + + All media items + + using this media type will be deleted permanently, please confirm you want to delete these as well. +

+ +
+ + + + + +
+
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js new file mode 100644 index 0000000000..ff9d412f8a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -0,0 +1,348 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.MediaType.EditController + * @function + * + * @description + * The controller for the media type editor + */ +(function () { + "use strict"; + + function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService, overlayHelper, eventsService) { + + var vm = this; + var localizeSaving = localizationService.localize("general_saving"); + var evts = []; + + vm.save = save; + + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.navigation = [ + { + "name": localizationService.localize("general_design"), + "icon": "icon-document-dashed-line", + "view": "views/mediatypes/views/design/design.html", + "active": true + }, + { + "name": localizationService.localize("general_listView"), + "icon": "icon-list", + "view": "views/mediatypes/views/listview/listview.html" + }, + { + "name": localizationService.localize("general_rights"), + "icon": "icon-keychain", + "view": "views/mediatypes/views/permissions/permissions.html" + } + ]; + + vm.page.keyboardShortcutsOverview = [ + { + "name": localizationService.localize("main_sections"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_navigateSections"), + "keys": [{ "key": "1" }, { "key": "3" }], + "keyRange": true + } + ] + }, + { + "name": localizationService.localize("general_design"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_addTab"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] + }, + { + "description": localizationService.localize("shortcuts_addProperty"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] + }, + { + "description": localizationService.localize("shortcuts_addEditor"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] + }, + { + "description": localizationService.localize("shortcuts_editDataType"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] + } + ] + }, + { + "name": localizationService.localize("general_listView"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_toggleListView"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] + } + ] + }, + { + "name": localizationService.localize("general_rights"), + "shortcuts": [ + { + "description": localizationService.localize("shortcuts_toggleAllowAsRoot"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] + }, + { + "description": localizationService.localize("shortcuts_addChildNode"), + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] + } + ] + } + ]; + + contentTypeHelper.checkModelsBuilderStatus().then(function (result) { + vm.page.modelsBuilder = result; + if (result) { + //Models builder mode: + vm.page.defaultButton = { + hotKey: "ctrl+s", + hotKeyWhenHidden: true, + labelKey: "buttons_save", + letter: "S", + type: "submit", + handler: function () { vm.save(); } + }; + vm.page.subButtons = [{ + hotKey: "ctrl+g", + hotKeyWhenHidden: true, + labelKey: "buttons_saveAndGenerateModels", + letter: "G", + handler: function () { + + vm.page.saveButtonState = "busy"; + + vm.save().then(function (result) { + + vm.page.saveButtonState = "busy"; + + localizationService.localize("modelsBuilder_buildingModels").then(function (headerValue) { + localizationService.localize("modelsBuilder_waitingMessage").then(function(msgValue) { + notificationsService.info(headerValue, msgValue); + }); + }); + + contentTypeHelper.generateModels().then(function (result) { + + if (result.success) { + + //re-check model status + contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { + vm.page.modelsBuilder = statusResult; + }); + + //clear and add success + vm.page.saveButtonState = "init"; + localizationService.localize("modelsBuilder_modelsGenerated").then(function(value) { + notificationsService.success(value); + }); + + } else { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsExceptionInUlog").then(function(value) { + notificationsService.error(value); + }); + } + + }, function () { + vm.page.saveButtonState = "error"; + localizationService.localize("modelsBuilder_modelsGeneratedError").then(function(value) { + notificationsService.error(value); + }); + }); + + }); + + } + }]; + } + }); + + if ($routeParams.create) { + vm.page.loading = true; + + //we are creating so get an empty data type item + mediaTypeResource.getScaffold($routeParams.id) + .then(function(dt) { + init(dt); + + vm.page.loading = false; + }); + } + else { + loadMediaType(); + } + + function loadMediaType() { + vm.page.loading = true; + + mediaTypeResource.getById($routeParams.id).then(function(dt) { + init(dt); + + syncTreeNode(vm.contentType, dt.path, true); + + vm.page.loading = false; + }); + } + + /* ---------- SAVE ---------- */ + + function save() { + + // only save if there is no overlays open + if(overlayHelper.getNumberOfOverlays() === 0) { + + var deferred = $q.defer(); + + vm.page.saveButtonState = "busy"; + + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: localizeSaving, + saveMethod: mediaTypeResource.save, + scope: $scope, + content: vm.contentType, + //We do not redirect on failure for doc types - this is because it is not possible to actually save the doc + // type 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, + // we need to rebind... the IDs that have been created! + rebindCallback: function (origContentType, savedContentType) { + vm.contentType.id = savedContentType.id; + vm.contentType.groups.forEach(function (group) { + if (!group.name) return; + + var k = 0; + while (k < savedContentType.groups.length && savedContentType.groups[k].name != group.name) + k++; + if (k == savedContentType.groups.length) { + group.id = 0; + return; + } + + var savedGroup = savedContentType.groups[k]; + if (!group.id) group.id = savedGroup.id; + + group.properties.forEach(function (property) { + if (property.id || !property.alias) return; + + k = 0; + while (k < savedGroup.properties.length && savedGroup.properties[k].alias != property.alias) + k++; + if (k == savedGroup.properties.length) { + property.id = 0; + return; + } + + var savedProperty = savedGroup.properties[k]; + property.id = savedProperty.id; + }); + }); + } + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); + + vm.page.saveButtonState = "success"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } + + vm.page.saveButtonState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; + } + } + + function init(contentType) { + + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { + + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); + + }); + } + + // convert icons for content type + convertLegacyIcons(contentType); + + //set a shared state + editorState.set(contentType); + + vm.contentType = contentType; + } + + function convertLegacyIcons(contentType) { + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + } + + function getDataTypeDetails(property) { + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function(dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + } + } + + + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { + navigationService.syncTree({ tree: "mediatypes", path: path.split(","), forceReload: initialLoad !== true }).then(function(syncArgs) { + vm.currentNode = syncArgs.node; + }); + } + + evts.push(eventsService.on("app.refreshEditor", function(name, error) { + loadMediaType(); + })); + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } + + angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html new file mode 100644 index 0000000000..5bde09f8e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html @@ -0,0 +1,69 @@ +
+ + + +
+ + + + + + + + +
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js new file mode 100644 index 0000000000..f9bb584de7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js @@ -0,0 +1,69 @@ +angular.module("umbraco") +.controller("Umbraco.Editors.MediaTypes.MoveController", + function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + mediaTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "mediaTypes", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + eventsService.emit('app.refreshEditor'); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html new file mode 100644 index 0000000000..59172eb547 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html @@ -0,0 +1,51 @@ +
+ +
+
+ +

+ Select the folder to move {{currentNode.name}} to in the tree structure below +

+ +
+
+
+ +
+
{{error.errorMsg}}
+

{{error.data.message}}

+
+ +
+
{{currentNode.name}} was moved underneath {{target.name}}
+ +
+ +
+ +
+ + +
+ +
+
+
+ + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/design/design.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/design/design.html new file mode 100644 index 0000000000..215664e4db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/design/design.html @@ -0,0 +1,4 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/listview/listview.html new file mode 100644 index 0000000000..1461341d6d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/listview/listview.html @@ -0,0 +1,19 @@ +
+ +
+
+ +
+ +
+ + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js new file mode 100644 index 0000000000..9d7cb8a1da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js @@ -0,0 +1,71 @@ +(function() { + 'use strict'; + + function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService) { + + /* ----------- SCOPE VARIABLES ----------- */ + + var vm = this; + var childNodeSelectorOverlayTitle = ""; + + vm.mediaTypes = []; + vm.selectedChildren = []; + + vm.addChild = addChild; + vm.removeChild = removeChild; + + /* ---------- INIT ---------- */ + + init(); + + function init() { + + childNodeSelectorOverlayTitle = localizationService.localize("contentTypeEditor_chooseChildNode"); + + mediaTypeResource.getAll().then(function(mediaTypes){ + + vm.mediaTypes = mediaTypes; + + // convert legacy icons + iconHelper.formatContentTypeIcons(vm.mediaTypes); + + vm.selectedChildren = contentTypeHelper.makeObjectArrayFromId($scope.model.allowedContentTypes, vm.mediaTypes); + + if($scope.model.id === 0) { + contentTypeHelper.insertChildNodePlaceholder(vm.mediaTypes, $scope.model.name, $scope.model.icon, $scope.model.id); + } + + }); + + } + + function addChild($event) { + vm.childNodeSelectorOverlay = { + view: "itempicker", + title: childNodeSelectorOverlayTitle, + availableItems: vm.mediaTypes, + selectedItems: vm.selectedChildren, + event: $event, + show: true, + submit: function(model) { + vm.selectedChildren.push(model.selectedItem); + $scope.model.allowedContentTypes.push(model.selectedItem.id); + vm.childNodeSelectorOverlay.show = false; + vm.childNodeSelectorOverlay = null; + } + }; + } + + function removeChild(selectedChild, index) { + // remove from vm + vm.selectedChildren.splice(index, 1); + + // remove from content type model + var selectedChildIndex = $scope.model.allowedContentTypes.indexOf(selectedChild.id); + $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); + } + + } + + angular.module("umbraco").controller("Umbraco.Editors.MediaType.PermissionsController", PermissionsController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html new file mode 100644 index 0000000000..7c9811e37a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html @@ -0,0 +1,49 @@ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+ +
+ + + + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/create.html b/src/Umbraco.Web.UI.Client/src/views/member/create.html index 03f0237e38..aa0e6c0f8d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/create.html @@ -1,4 +1,4 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html index edc2db2f74..561872cb2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html @@ -1,13 +1,13 @@
- - + +
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/number.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/number.html index 6d7b8c4c78..e7490e2703 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/number.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/number.html @@ -3,7 +3,7 @@ type="number" ng-model="model.value" val-server="value" - fix-number /> + fix-number /> Not a number {{propertyForm.requiredField.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js index 91a228267b..d2ff7bd778 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js @@ -24,16 +24,33 @@ angular.module('umbraco') }); }); } - - $scope.openContentPicker =function() { - var d = dialogService.treePicker({ - section: config.type, - treeAlias: config.treeAlias, - multiPicker: config.multiPicker, - callback: populate - }); - }; + $scope.openContentPicker = function() { + $scope.treePickerOverlay = {}; + $scope.treePickerOverlay.section = config.type; + $scope.treePickerOverlay.treeAlias = config.treeAlias; + $scope.treePickerOverlay.multiPicker = config.multiPicker; + $scope.treePickerOverlay.view = "treePicker"; + $scope.treePickerOverlay.show = true; + + $scope.treePickerOverlay.submit = function(model) { + + if(config.multiPicker) { + populate(model.selection); + } else { + populate(model.selection[0]); + } + + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + }; + + $scope.treePickerOverlay.close = function(oldModel) { + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + }; + + } $scope.remove =function(index){ $scope.renderModel.splice(index, 1); @@ -82,4 +99,4 @@ angular.module('umbraco') $scope.add(data); } } -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html index 71ce43b883..fc7a388cc1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html @@ -19,4 +19,12 @@ -
\ No newline at end of file + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js index 087447b184..ff5e31b8eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js @@ -28,11 +28,19 @@ angular.module('umbraco') $scope.openContentPicker =function(){ - var d = dialogService.treePicker({ - section: $scope.model.value.type, - treeAlias: $scope.model.value.type, - multiPicker: false, - callback: populate}); + $scope.treePickerOverlay = { + view: "treepicker", + section: $scope.model.value.type, + treeAlias: $scope.model.value.type, + multiPicker: false, + show: true, + submit: function(model) { + var item = model.selection[0]; + populate(item); + $scope.treePickerOverlay.show = false; + $scope.treePickerOverlay = null; + } + }; }; $scope.clear = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html index 8811b549b5..08bd4913f6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html @@ -6,9 +6,11 @@ -
    -
  • - + - \ No newline at end of file + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/valuetype.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/valuetype.html new file mode 100644 index 0000000000..59b7c55c19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/valuetype.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js index 1891e628c6..422f761851 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -4,6 +4,11 @@ function booleanEditorController($scope, $rootScope, assetsService) { $scope.renderModel = { value: false }; + + if ($scope.model.config && $scope.model.config.default && $scope.model.config.default.toString() === "1" && $scope.model && !$scope.model.value) { + $scope.renderModel.value = true; + } + if ($scope.model && $scope.model.value && ($scope.model.value.toString() === "1" || angular.lowercase($scope.model.value) === "true")) { $scope.renderModel.value = true; } @@ -14,7 +19,7 @@ function booleanEditorController($scope, $rootScope, assetsService) { $scope.$watch("renderModel.value", function (newVal) { $scope.model.value = newVal === true ? "1" : "0"; }); - + //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { @@ -23,4 +28,4 @@ function booleanEditorController($scope, $rootScope, assetsService) { }; } -angular.module("umbraco").controller("Umbraco.PropertyEditors.BooleanController", booleanEditorController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.PropertyEditors.BooleanController", booleanEditorController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html index 8d44b008a1..b2373f2d12 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html @@ -1,3 +1,3 @@
    -
    + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html index c6c9bb115f..78ae6afa6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html @@ -5,12 +5,14 @@ {{model.value.generatedPassword}}
    -
    - Change password +
    - + - + - + - + + autocomplete="off"/> - Passwords must match - + + The confirmed password doesn't match the new password! + + - Cancel + + Cancel +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html index 82236c96a5..d5ea996299 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html @@ -1,12 +1,12 @@ 
    + -
    {{item.value}}
    - +
    -
    \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 48f4ed5bda..667dfd0f22 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -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 -function contentPickerController($scope, dialogService, entityResource, editorState, $log, iconHelper, $routeParams, fileManager, contentEditingHelper) { +function contentPickerController($scope, dialogService, entityResource, editorState, $log, iconHelper, $routeParams, fileManager, contentEditingHelper, angularHelper, navigationService, $location) { function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -49,7 +49,9 @@ function contentPickerController($scope, dialogService, entityResource, editorSt //the default pre-values var defaultConfig = { multiPicker: false, - showEditButton: false, + showOpenButton: false, + showEditButton: false, + showPathOnHover: false, startNode: { query: "", type: "content", @@ -64,13 +66,17 @@ function contentPickerController($scope, dialogService, entityResource, editorSt //Umbraco persists boolean for prevalues as "0" or "1" so we need to convert that! $scope.model.config.multiPicker = ($scope.model.config.multiPicker === "1" ? true : false); + $scope.model.config.showOpenButton = ($scope.model.config.showOpenButton === "1" ? true : false); $scope.model.config.showEditButton = ($scope.model.config.showEditButton === "1" ? true : false); - + $scope.model.config.showPathOnHover = ($scope.model.config.showPathOnHover === "1" ? true : false); + var entityType = $scope.model.config.startNode.type === "member" ? "Member" : $scope.model.config.startNode.type === "media" ? "Media" : "Document"; + $scope.allowOpenButton = entityType === "Document" || entityType === "Media"; + $scope.allowEditButton = entityType === "Document"; //the dialog options for the picker var dialogOptions = { @@ -87,6 +93,7 @@ function contentPickerController($scope, dialogService, entityResource, editorSt $scope.clear(); $scope.add(data); } + angularHelper.getCurrentForm($scope).$setDirty(); }, treeAlias: $scope.model.config.startNode.type, section: $scope.model.config.startNode.type @@ -132,17 +139,53 @@ function contentPickerController($scope, dialogService, entityResource, editorSt }); } else { dialogOptions.startNodeId = $scope.model.config.startNode.id; - } - + } + //dialog - $scope.openContentPicker = function () { - var d = dialogService.treePicker(dialogOptions); + $scope.openContentPicker = function() { + $scope.contentPickerOverlay = dialogOptions; + $scope.contentPickerOverlay.view = "treepicker"; + $scope.contentPickerOverlay.show = true; + + $scope.contentPickerOverlay.submit = function(model) { + + if (angular.isArray(model.selection)) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + } + + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + } + + $scope.contentPickerOverlay.close = function(oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + } + }; $scope.remove = function (index) { $scope.renderModel.splice(index, 1); + angularHelper.getCurrentForm($scope).$setDirty(); }; - + + $scope.showNode = function (index) { + var item = $scope.renderModel[index]; + var id = item.id; + var section = $scope.model.config.startNode.type.toLowerCase(); + + entityResource.getPath(id, entityType).then(function (path) { + navigationService.changeSection(section); + navigationService.showTree(section, { + tree: section, path: path, forceReload: false, activate: true + }); + var routePath = section + "/" + section + "/edit/" + id.toString(); + $location.path(routePath).search(""); + }); + } + $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { return i.id; @@ -150,7 +193,7 @@ function contentPickerController($scope, dialogService, entityResource, editorSt if (currIds.indexOf(item.id) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon }); + $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, path: item.path }); } }; @@ -182,7 +225,7 @@ function contentPickerController($scope, dialogService, entityResource, editorSt if (entity) { entity.icon = iconHelper.convertFromLegacyIcon(entity.icon); - $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon }); + $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon, path: entity.path }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index c5de0a69db..9f5f3fb60f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -1,23 +1,22 @@
    - + - @@ -29,7 +28,7 @@
- + @@ -37,11 +36,19 @@
You need to add at least {{model.config.minNumber}} items
- +
You can only have {{model.config.maxNumber}} items selected
- \ No newline at end of file + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index fe413e0159..693bd49ec6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -44,6 +44,7 @@ function dateTimePickerController($scope, notificationsService, assetsService, a $scope.datePickerForm.datepicker.$setValidity("pickerError", true); $scope.hasDatetimePickerValue = true; $scope.datetimePickerValue = e.date.format($scope.model.config.format); + $scope.model.value = $scope.datetimePickerValue; } else { $scope.hasDatetimePickerValue = false; @@ -96,11 +97,11 @@ function dateTimePickerController($scope, notificationsService, assetsService, a }); if ($scope.hasDatetimePickerValue) { - //assign value to plugin/picker - var dateVal = $scope.model.value ? moment($scope.model.value, $scope.model.config.format) : moment(); + var dateVal = $scope.model.value ? moment($scope.model.value, "YYYY-MM-DD HH:mm:ss") : moment(); + element.datetimepicker("setValue", dateVal); - $scope.datetimePickerValue = moment($scope.model.value).format($scope.model.config.format); + $scope.datetimePickerValue = dateVal.format($scope.model.config.format); } element.find("input").bind("blur", function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 898afe88a3..e6d49e5c6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -2,7 +2,7 @@
- + + + Not a number + {{propertyForm.requiredField.errorMsg}} +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 0081d6ac26..8ccf5a6df1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,6 +1,5 @@ 
- - Required Invalid email -
+ \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 37dea64c21..4285cf0f77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -2,16 +2,16 @@ * @ngdoc controller * @name Umbraco.Editors.FileUploadController * @function - * + * * @description * The controller for the file upload property editor. It is important to note that the $scope.model.value - * doesn't necessarily depict what is saved for this property editor. $scope.model.value can be empty when we + * doesn't necessarily depict what is saved for this property editor. $scope.model.value can be empty when we * are submitting files because in that case, we are adding files to the fileManager which is what gets peristed * on the server. However, when we are clearing files, we are setting $scope.model.value to "{clearFiles: true}" - * to indicate on the server that we are removing files for this property. We will keep the $scope.model.value to + * to indicate on the server that we are removing files for this property. We will keep the $scope.model.value to * be the name of the file selected (if it is a newly selected file) or keep it to be it's original value, this allows * for the editors to check if the value has changed and to re-bind the property if that is true. - * + * */ function fileUploadController($scope, $element, $compile, imageHelper, fileManager, umbRequestHelper, mediaHelper) { @@ -21,11 +21,14 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag fileManager.setFiles($scope.model.alias, []); //clear the current files $scope.files = []; - if ($scope.propertyForm.fileCount) { - //this is required to re-validate - $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + + if($scope.propertyForm) { + if ($scope.propertyForm.fileCount) { + //this is required to re-validate + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + } } - + } /** this method is used to initialize the data and to re-initialize it if the server value is changed */ @@ -37,7 +40,7 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag index = 1; } - //this is used in order to tell the umb-single-file-upload directive to + //this is used in order to tell the umb-single-file-upload directive to //rebuild the html input control (and thus clearing the selected file) since //that is the only way to manipulate the html for the file input control. $scope.rebuildInput = { @@ -49,7 +52,7 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag $scope.originalValue = $scope.model.value; //create the property to show the list of files currently saved - if ($scope.model.value != "") { + if ($scope.model.value != "" && $scope.model.value != undefined) { var images = $scope.model.value.split(","); @@ -95,7 +98,9 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag //reset to original value $scope.model.value = $scope.originalValue; //this is required to re-validate - $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + if($scope.propertyForm) { + $scope.propertyForm.fileCount.$setViewValue($scope.files.length); + } } }); @@ -127,12 +132,12 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag //listen for when the model value has changed $scope.$watch("model.value", function (newVal, oldVal) { - //cannot just check for !newVal because it might be an empty string which we + //cannot just check for !newVal because it might be an empty string which we //want to look for. if (newVal !== null && newVal !== undefined && newVal !== oldVal) { //now we need to check if we need to re-initialize our structure which is kind of tricky // since we only want to do that if the server has changed the value, not if this controller - // has changed the value. There's only 2 scenarios where we change the value internall so + // has changed the value. There's only 2 scenarios where we change the value internall so // we know what those values can be, if they are not either of them, then we'll re-initialize. if (newVal.clearFiles !== true && newVal !== $scope.originalValue && !newVal.selectedFiles) { @@ -144,31 +149,28 @@ function fileUploadController($scope, $element, $compile, imageHelper, fileManag }; angular.module("umbraco") .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) - .run(function(mediaHelper, umbRequestHelper){ + .run(function(mediaHelper, umbRequestHelper, assetsService){ if (mediaHelper && mediaHelper.registerFileResolver) { - - //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource - // they contain different data structures so if we need to query against it we need to be aware of this. - mediaHelper.registerFileResolver("Umbraco.UploadField", function(property, entity, thumbnail){ - if (thumbnail) { - - if (mediaHelper.detectIfImageByExtension(property.value)) { - - var thumbnailUrl = umbRequestHelper.getApiUrl( - "imagesApiBaseUrl", - "GetBigThumbnail", - [{ originalImagePath: property.value }]); - - return thumbnailUrl; - } - else { - return null; - } - + assetsService.load(["lib/moment/moment-with-locales.js"]).then( + function () { + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource + // they contain different data structures so if we need to query against it we need to be aware of this. + mediaHelper.registerFileResolver("Umbraco.UploadField", function(property, entity, thumbnail){ + if (thumbnail) { + if (mediaHelper.detectIfImageByExtension(property.value)) { + //get default big thumbnail from image processor + var thumbnailUrl = property.value + "?rnd=" + moment(entity.updateDate).format("YYYYMMDDHHmmss") + "&width=500&animationprocessmode=first"; + return thumbnailUrl; + } + else { + return null; + } + } + else { + return property.value; + } + }); } - else { - return property.value; - } - }); + ); } - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index e200b2726c..c213378b23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -1,19 +1,19 @@ 
- -
+ +
- +
@@ -23,9 +23,9 @@
  • {{file.file.name}}
  • -
+
- \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.controller.js index 5b76cbae81..5e0cc40db9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.controller.js @@ -1,20 +1,29 @@ angular.module("umbraco") +//this controller is obsolete and should not be used anymore +//it proxies everything to the system media list view which has overtaken +//all the work this property editor used to perform .controller("Umbraco.PropertyEditors.FolderBrowserController", - function ($rootScope, $scope, $routeParams, $timeout, editorState, navigationService) { + function ($rootScope, $scope, contentTypeResource) { + //get the system media listview + contentTypeResource.getPropertyTypeScaffold(-96) + .then(function(dt) { - var dialogOptions = $scope.dialogOptions; - $scope.creating = $routeParams.create; - $scope.nodeId = $routeParams.id; + $scope.fakeProperty = { + alias: "contents", + config: dt.config, + description: "", + editor: dt.editor, + hideLabel: true, + id: 1, + label: "Contents:", + validation: { + mandatory: false, + pattern: null + }, + value: "", + view: dt.view + }; - $scope.onUploadComplete = function () { - - //sync the tree - don't force reload since we're not updating this particular node (i.e. its name or anything), - // then we'll get the resulting tree node which we can then use to reload it's children. - var path = editorState.current.path; - navigationService.syncTree({ tree: "media", path: path, forceReload: false }).then(function (syncArgs) { - navigationService.reloadNode(syncArgs.node); - }); - - } + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.html index 4e3814b9ab..76b362f2c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/folderbrowser/folderbrowser.html @@ -1,5 +1,3 @@
- -
\ No newline at end of file + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/additem.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/additem.html deleted file mode 100644 index 77e2c27bd5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/additem.html +++ /dev/null @@ -1,16 +0,0 @@ -
-
- - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js deleted file mode 100644 index d60535acd1..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.controller.js +++ /dev/null @@ -1,77 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridController.Dialogs.Config", - function ($scope, $http) { - - var placeHolder = "{0}"; - var addModifier = function(val, modifier){ - if (!modifier || modifier.indexOf(placeHolder) < 0) { - return val; - } else { - return modifier.replace(placeHolder, val); - } - } - - var stripModifier = function (val, modifier) { - if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { - return val; - } else { - var paddArray = modifier.split(placeHolder); - if(paddArray.length == 1){ - if (modifier.indexOf(placeHolder) === 0) { - return val.slice(0, -paddArray[0].length); - } else { - return val.slice(paddArray[0].length, 0); - } - } else { - if (paddArray[1].length === 0) { - return val.slice(paddArray[0].length); - } - return val.slice(paddArray[0].length, -paddArray[1].length); - } - } - } - - - $scope.styles = _.filter( angular.copy($scope.dialogOptions.config.items.styles), function(item){return (item.applyTo === undefined || item.applyTo === $scope.dialogOptions.itemType); }); - $scope.config = _.filter( angular.copy($scope.dialogOptions.config.items.config), function(item){return (item.applyTo === undefined || item.applyTo === $scope.dialogOptions.itemType); }); - - - var element = $scope.dialogOptions.gridItem; - if(angular.isObject(element.config)){ - _.each($scope.config, function(cfg){ - var val = element.config[cfg.key]; - if(val){ - cfg.value = stripModifier(val, cfg.modifier); - } - }); - } - - if(angular.isObject(element.styles)){ - _.each($scope.styles, function(style){ - var val = element.styles[style.key]; - if(val){ - style.value = stripModifier(val, style.modifier); - } - }); - } - - - $scope.saveAndClose = function(){ - var styleObject = {}; - var configObject = {}; - - _.each($scope.styles, function(style){ - if(style.value){ - styleObject[style.key] = addModifier(style.value, style.modifier); - } - }); - _.each($scope.config, function (cfg) { - if (cfg.value) { - configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); - } - }); - - $scope.submit({config: configObject, styles: styleObject}); - }; - - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html index 429bd68abd..aa96348130 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/config.html @@ -1,44 +1,16 @@ - -
- - - - - +
+ + + +
+ +
+

Style

+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.controller.js deleted file mode 100644 index f1016f6c79..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.controller.js +++ /dev/null @@ -1,15 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.GridPrevalueEditorController.Dialogs.EditConfig", - function ($scope, $http) { - - $scope.renderModel = {}; - $scope.renderModel.name = $scope.dialogOptions.name; - $scope.renderModel.config = $scope.dialogOptions.config; - - $scope.saveAndClose = function(isValid){ - if(isValid){ - $scope.submit($scope.renderModel.config); - } - }; - - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html index 61465f95ae..1c67eb0212 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/editconfig.html @@ -1,35 +1,14 @@
-
- - - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js index 628112903d..e886cb977e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js @@ -2,9 +2,9 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GridPrevalueEditor.LayoutConfigController", function ($scope) { - $scope.currentLayout = $scope.dialogOptions.currentLayout; - $scope.columns = $scope.dialogOptions.columns; - $scope.rows = $scope.dialogOptions.rows; + $scope.currentLayout = $scope.model.currentLayout; + $scope.columns = $scope.model.columns; + $scope.rows = $scope.model.rows; $scope.scaleUp = function(section, max, overflow){ var add = 1; @@ -68,4 +68,4 @@ angular.module("umbraco") $scope.availableLayoutSpace = $scope.columns - total; } }, true); - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html index de17dcf638..9cbe74bae2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html @@ -1,116 +1,108 @@ -
- diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js index 7f7b817b8a..09fa263354 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js @@ -1,8 +1,8 @@ function RowConfigController($scope) { - - $scope.currentRow = angular.copy($scope.dialogOptions.currentRow); - $scope.editors = $scope.dialogOptions.editors; - $scope.columns = $scope.dialogOptions.columns; + + $scope.currentRow = $scope.model.currentRow; + $scope.editors = $scope.model.editors; + $scope.columns = $scope.model.columns; $scope.scaleUp = function(section, max, overflow) { var add = 1; @@ -88,10 +88,6 @@ function RowConfigController($scope) { } }, true); - $scope.complete = function () { - angular.extend($scope.dialogOptions.currentRow, $scope.currentRow); - $scope.close(); - } } -angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController", RowConfigController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.PropertyEditors.GridPrevalueEditor.RowConfigController", RowConfigController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index 63e250645f..b1343a960b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -1,102 +1,87 @@ -
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html index db416396a1..c69ae2cb8f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowdeleteconfirm.html @@ -1,26 +1,18 @@ -
- \ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js index d6abe24974..dfc9cfdcef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js @@ -1,13 +1,23 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.EmbedController", - function ($scope, $rootScope, $timeout, dialogService) { + function ($scope, $rootScope, $timeout) { $scope.setEmbed = function(){ - dialogService.embedDialog({ - callback: function (data) { - $scope.control.value = data; - } - }); + $scope.embedDialog = {}; + $scope.embedDialog.view = "embed"; + $scope.embedDialog.show = true; + + $scope.embedDialog.submit = function(model) { + $scope.control.value = model.embed.preview; + $scope.embedDialog.show = false; + $scope.embedDialog = null; + }; + + $scope.embedDialog.close = function(oldModel) { + $scope.embedDialog.show = false; + $scope.embedDialog = null; + }; + }; $timeout(function(){ @@ -16,4 +26,3 @@ angular.module("umbraco") } }, 200); }); - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html index f2c49e8f39..e506ac5880 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html @@ -1,10 +1,17 @@
-
+
Click to embed
-
+ + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js index ac5399cb75..d536d9174a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js @@ -5,23 +5,40 @@ angular.module("umbraco") $scope.title = "Click to insert macro"; $scope.setMacro = function(){ - dialogService.macroPicker({ - dialogData: { - richTextEditor: true, - macroData: $scope.control.value || { - macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias - ? $scope.control.editor.config.macroAlias : "" - } - }, - callback: function (data) { - $scope.control.value = { - macroAlias: data.macroAlias, - macroParamsDictionary: data.macroParamsDictionary - }; - $scope.setPreview($scope.control.value ); + var dialogData = { + richTextEditor: true, + macroData: $scope.control.value || { + macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias + ? $scope.control.editor.config.macroAlias : "" } - }); + }; + + $scope.macroPickerOverlay = {}; + $scope.macroPickerOverlay.view = "macropicker"; + $scope.macroPickerOverlay.dialogData = dialogData; + $scope.macroPickerOverlay.show = true; + + $scope.macroPickerOverlay.submit = function(model) { + + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + + $scope.control.value = { + macroAlias: macroObject.macroAlias, + macroParamsDictionary: macroObject.macroParamsDictionary + }; + + $scope.setPreview($scope.control.value ); + + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + + $scope.macroPickerOverlay.close = function(oldModel) { + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + }; $scope.setPreview = function(macro){ @@ -45,4 +62,3 @@ angular.module("umbraco") } }, 200); }); - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html index ad646b361c..732b551b52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html @@ -1,6 +1,6 @@
-
+
{{title}}
@@ -13,5 +13,11 @@
-
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index 69a85957d9..e289a389cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,26 +1,36 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", - function ($scope, $rootScope, $timeout, dialogService) { + function ($scope, $rootScope, $timeout) { $scope.setImage = function(){ + $scope.mediaPickerOverlay = {}; + $scope.mediaPickerOverlay.view = "mediapicker"; + $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; + $scope.mediaPickerOverlay.showDetails = true; + $scope.mediaPickerOverlay.disableFolderSelect = true; + $scope.mediaPickerOverlay.onlyImages = true; + $scope.mediaPickerOverlay.show = true; - dialogService.mediaPicker({ - startNodeId: $scope.control.editor.config && $scope.control.editor.config.startNodeId ? $scope.control.editor.config.startNodeId : undefined, - multiPicker: false, - cropSize: $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined, - showDetails: true, - callback: function (data) { + $scope.mediaPickerOverlay.submit = function(model) { + var selectedImage = model.selectedImages[0]; - $scope.control.value = { - focalPoint: data.focalPoint, - id: data.id, - image: data.image, - altText: data.altText - }; + $scope.control.value = { + focalPoint: selectedImage.focalPoint, + id: selectedImage.id, + image: selectedImage.image, + altText: selectedImage.altText + }; - $scope.setUrl(); - } - }); + $scope.setUrl(); + + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + + $scope.mediaPickerOverlay.close = function(oldModel) { + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; }; $scope.setUrl = function(){ @@ -31,6 +41,7 @@ angular.module("umbraco") if($scope.control.editor.config && $scope.control.editor.config.size){ url += "?width=" + $scope.control.editor.config.size.width; url += "&height=" + $scope.control.editor.config.size.height; + url += "&animationprocessmode=first"; if($scope.control.value.focalPoint){ url += "¢er=" + $scope.control.value.focalPoint.top +"," + $scope.control.value.focalPoint.left; @@ -38,6 +49,11 @@ angular.module("umbraco") } } + // set default size if no crop present (moved from the view) + if (url.indexOf('?') == -1) + { + url += "?width=800&upscale=false&animationprocessmode=false" + } $scope.url = url; } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html index efb6c3e290..dd35757397 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html @@ -1,6 +1,6 @@
-
+
Click to insert image
@@ -13,4 +13,12 @@ class="fullSizeImage" />
+ + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js new file mode 100644 index 0000000000..e406f87aed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -0,0 +1,74 @@ +(function() { + "use strict"; + + function GridRichTextEditorController($scope, tinyMceService, macroService) { + + var vm = this; + + vm.openLinkPicker = openLinkPicker; + vm.openMediaPicker = openMediaPicker; + vm.openMacroPicker = openMacroPicker; + vm.openEmbed = openEmbed; + + function openLinkPicker(editor, currentTarget, anchorElement) { + vm.linkPickerOverlay = { + view: "linkpicker", + currentTarget: currentTarget, + show: true, + submit: function(model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + vm.linkPickerOverlay.show = false; + vm.linkPickerOverlay = null; + } + }; + } + + function openMediaPicker(editor, currentTarget, userData) { + vm.mediaPickerOverlay = { + currentTarget: currentTarget, + onlyImages: true, + showDetails: true, + startNodeId: userData.startMediaId, + view: "mediapicker", + show: true, + submit: function(model) { + tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); + vm.mediaPickerOverlay.show = false; + vm.mediaPickerOverlay = null; + } + }; + } + + function openEmbed(editor) { + vm.embedOverlay = { + view: "embed", + show: true, + submit: function(model) { + tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); + vm.embedOverlay.show = false; + vm.embedOverlay = null; + } + }; + } + + function openMacroPicker(editor, dialogData) { + vm.macroPickerOverlay = { + view: "macropicker", + dialogData: dialogData, + show: true, + submit: function(model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + tinyMceService.insertMacroInEditor(editor, macroObject, $scope); + vm.macroPickerOverlay.show = false; + vm.macroPickerOverlay = null; + } + }; + } + + + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.Grid.RichTextEditorController", GridRichTextEditorController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html index 93229ecd4a..7bceed6a60 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html @@ -1,5 +1,42 @@ -
+
+ +
+
+ + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 24aa722cc3..54155574f1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -1,14 +1,20 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GridController", - function ($scope, $http, assetsService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper) { + function ($scope, $http, assetsService, localizationService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper, angularHelper) { // Grid status variables + var placeHolder = ""; + var currentForm = angularHelper.getCurrentForm($scope); + $scope.currentRow = null; $scope.currentCell = null; $scope.currentToolsControl = null; $scope.currentControl = null; $scope.openRTEToolbarId = null; $scope.hasSettings = false; + $scope.showRowConfigurations = true; + $scope.sortMode = false; + $scope.reorderKey = "general_reorder"; // ********************************************* // Sortable options @@ -16,23 +22,24 @@ angular.module("umbraco") var draggedRteSettings; - $scope.sortableOptions = { + $scope.sortableOptionsRow = { distance: 10, cursor: "move", placeholder: "ui-sortable-placeholder", - handle: ".cell-tools-move", + handle: ".umb-row-title-bar", + helper: "clone", forcePlaceholderSize: true, tolerance: "pointer", zIndex: 999999999999999999, scrollSensitivity: 100, cursorAt: { - top: 45, - left: 90 + top: 40, + left: 60 }, sort: function (event, ui) { /* prevent vertical scroll out of the screen */ - var max = $(".usky-grid").width() - 150; + var max = $(".umb-grid").width() - 150; if (parseInt(ui.helper.css("left")) > max) { ui.helper.css({ "left": max + "px" }); } @@ -42,23 +49,33 @@ angular.module("umbraco") }, start: function (e, ui) { + + // Fade out row when sorting + ui.item.context.style.display = "block"; + ui.item.context.style.opacity = "0.5"; + draggedRteSettings = {}; ui.item.find(".mceNoEditor").each(function () { // remove all RTEs in the dragged row and save their settings var id = $(this).attr("id"); draggedRteSettings[id] = _.findWhere(tinyMCE.editors, { id: id }).settings; - tinyMCE.execCommand("mceRemoveEditor", false, id); + // tinyMCE.execCommand("mceRemoveEditor", false, id); }); }, stop: function (e, ui) { + + // Fade in row when sorting stops + ui.item.context.style.opacity = "1"; + // reset all RTEs affected by the dragging - ui.item.parents(".usky-column").find(".mceNoEditor").each(function () { + ui.item.parents(".umb-column").find(".mceNoEditor").each(function () { var id = $(this).attr("id"); draggedRteSettings[id] = draggedRteSettings[id] || _.findWhere(tinyMCE.editors, { id: id }).settings; tinyMCE.execCommand("mceRemoveEditor", false, id); tinyMCE.init(draggedRteSettings[id]); }); + currentForm.$setDirty(); } }; @@ -69,8 +86,9 @@ angular.module("umbraco") distance: 10, cursor: "move", placeholder: "ui-sortable-placeholder", - handle: ".cell-tools-move", - connectWith: ".usky-cell", + handle: ".umb-control-handle", + helper: "clone", + connectWith: ".umb-cell-inner", forcePlaceholderSize: true, tolerance: "pointer", zIndex: 999999999999999999, @@ -81,14 +99,15 @@ angular.module("umbraco") }, sort: function (event, ui) { + /* prevent vertical scroll out of the screen */ - var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css("left")) - parseInt($(".usky-grid").offset().left); - var max = $(".usky-grid").width() - 220; + var position = parseInt(ui.item.parent().offset().left) + parseInt(ui.helper.css("left")) - parseInt($(".umb-grid").offset().left); + var max = $(".umb-grid").width() - 220; if (position > max) { - ui.helper.css({ "left": max - parseInt(ui.item.parent().offset().left) + parseInt($(".usky-grid").offset().left) + "px" }); + ui.helper.css({ "left": max - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); } if (position < 0) { - ui.helper.css({ "left": 0 - parseInt(ui.item.parent().offset().left) + parseInt($(".usky-grid").offset().left) + "px" }); + ui.helper.css({ "left": 0 - parseInt(ui.item.parent().offset().left) + parseInt($(".umb-grid").offset().left) + "px" }); } }, @@ -96,23 +115,42 @@ angular.module("umbraco") var allowedEditors = $(event.target).scope().area.allowed; if ($.inArray(ui.item.scope().control.editor.alias, allowedEditors) < 0 && allowedEditors) { + + $scope.$apply(function () { + $(event.target).scope().area.dropNotAllowed = true; + }); + ui.placeholder.hide(); cancelMove = true; } else { - ui.placeholder.show(); + if ($(event.target).scope().area.controls.length == 0){ + + $scope.$apply(function () { + $(event.target).scope().area.dropOnEmpty = true; + }); + ui.placeholder.hide(); + } else { + ui.placeholder.show(); + } cancelMove = false; } + }, + out: function(event, ui) { + $scope.$apply(function () { + $(event.target).scope().area.dropNotAllowed = false; + $(event.target).scope().area.dropOnEmpty = false; + }); }, update: function (event, ui) { - // add all RTEs which are affected by the dragging + /* add all RTEs which are affected by the dragging */ if (!ui.sender) { if (cancelMove) { ui.item.sortable.cancel(); } - ui.item.parents(".usky-cell").find(".mceNoEditor").each(function () { + ui.item.parents(".umb-cell.content").find(".mceNoEditor").each(function () { if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { notIncludedRte.splice(0, 0, $(this).attr("id")); } @@ -125,26 +163,40 @@ angular.module("umbraco") } }); } - + currentForm.$setDirty(); }, start: function (e, ui) { + + // fade out control when sorting + ui.item.context.style.display = "block"; + ui.item.context.style.opacity = "0.5"; + // reset dragged RTE settings in case a RTE isn't dragged draggedRteSettings = undefined; - + ui.item.context.style.display = "block"; ui.item.find(".mceNoEditor").each(function () { notIncludedRte = []; + var editors = _.findWhere(tinyMCE.editors, { id: $(this).attr("id") }); // save the dragged RTE settings - draggedRteSettings = _.findWhere(tinyMCE.editors, { id: $(this).attr("id") }).settings; + if(editors) { + draggedRteSettings = editors.settings; + + // remove the dragged RTE + tinyMCE.execCommand("mceRemoveEditor", false, $(this).attr("id")); + + } - // remove the dragged RTE - tinyMCE.execCommand("mceRemoveEditor", false, $(this).attr("id")); }); }, stop: function (e, ui) { - ui.item.parents(".usky-cell").find(".mceNoEditor").each(function () { + + // Fade in control when sorting stops + ui.item.context.style.opacity = "1"; + + ui.item.parents(".umb-cell-content").find(".mceNoEditor").each(function () { if ($.inArray($(this).attr("id"), notIncludedRte) < 0) { // add all dragged's neighbouring RTEs in the new cell notIncludedRte.splice(0, 0, $(this).attr("id")); @@ -165,45 +217,55 @@ angular.module("umbraco") } }); }, 500, false); + + $scope.$apply(function () { + + var cell = $(e.target).scope().area; + cell.hasActiveChild = hasActiveChild(cell, cell.controls); + cell.active = false; + }); } }; + $scope.toggleSortMode = function() { + $scope.sortMode = !$scope.sortMode; + if($scope.sortMode) { + $scope.reorderKey = "general_reorderDone"; + } else { + $scope.reorderKey = "general_reorder"; + } + }; + + $scope.showReorderButton = function() { + if($scope.model.value && $scope.model.value.sections) { + for(var i = 0; $scope.model.value.sections.length > i; i++) { + var section = $scope.model.value.sections[i]; + if(section.rows && section.rows.length > 0) { + return true; + } + } + } + }; + // ********************************************* // Add items overlay menu // ********************************************* - $scope.overlayMenu = { - show: false, - style: {}, - area: undefined, - key: undefined - }; - - $scope.addItemOverlay = function (event, area, index, key) { - $scope.overlayMenu.area = area; - $scope.overlayMenu.index = index; - $scope.overlayMenu.style = {}; - $scope.overlayMenu.key = key; - - //todo calculate position... - var offset = $(event.target).offset(); - var height = $(window).height(); - - if ((height - offset.top) < 250) { - $scope.overlayMenu.style.bottom = 0; - $scope.overlayMenu.style.top = "initial"; - } else if (offset.top < 300) { - $scope.overlayMenu.style.top = 190; - } - - $scope.overlayMenu.show = true; - }; - - $scope.closeItemOverlay = function () { - $scope.currentControl = null; - $scope.overlayMenu.show = false; - $scope.overlayMenu.key = undefined; - }; + $scope.openEditorOverlay = function(event, area, index, key) { + $scope.editorOverlay = { + view: "itempicker", + filter: false, + title: localizationService.localize("grid_insertControl"), + availableItems: area.$allowedEditors, + event: event, + show: true, + submit: function(model) { + $scope.addControl(model.selectedItem, area, index); + $scope.editorOverlay.show = false; + $scope.editorOverlay = null; + } + }; + }; // ********************************************* // Template management functions @@ -223,28 +285,12 @@ angular.module("umbraco") // Row management function // ********************************************* - $scope.setCurrentRow = function (row) { - $scope.currentRow = row; + $scope.clickRow = function(index, rows) { + rows[index].active = true; }; - $scope.disableCurrentRow = function () { - $scope.currentRow = null; - }; - - $scope.setWarnighlightRow = function (row) { - $scope.currentWarnhighlightRow = row; - }; - - $scope.disableWarnhighlightRow = function () { - $scope.currentWarnhighlightRow = null; - }; - - $scope.setInfohighlightRow = function (row) { - $scope.currentInfohighlightRow = row; - }; - - $scope.disableInfohighlightRow = function () { - $scope.currentInfohighlightRow = null; + $scope.clickOutsideRow = function(index, rows) { + rows[index].active = false; }; function getAllowedLayouts(section) { @@ -277,6 +323,11 @@ angular.module("umbraco") if (row) { section.rows.push(row); } + + currentForm.$setDirty(); + + $scope.showRowConfigurations = false; + }; $scope.removeRow = function (section, $index) { @@ -284,39 +335,136 @@ angular.module("umbraco") section.rows.splice($index, 1); $scope.currentRow = null; $scope.openRTEToolbarId = null; + currentForm.$setDirty(); + } - //$scope.initContent(); + if(section.rows.length === 0) { + $scope.showRowConfigurations = true; } }; $scope.editGridItemSettings = function (gridItem, itemType) { - dialogService.open( - { - template: "views/propertyeditors/grid/dialogs/config.html", - gridItem: gridItem, - config: $scope.model.config, - itemType: itemType, - callback: function (data) { + placeHolder = "{0}"; - gridItem.styles = data.styles; - gridItem.config = data.config; + var styles, config; + if (itemType === 'control') { + styles = null; + config = angular.copy(gridItem.editor.config.settings); + } else { + styles = _.filter(angular.copy($scope.model.config.items.styles), function (item) { return (item.applyTo === undefined || item.applyTo === itemType); }); + config = _.filter(angular.copy($scope.model.config.items.config), function (item) { return (item.applyTo === undefined || item.applyTo === itemType); }); + } + if(angular.isObject(gridItem.config)){ + _.each(config, function(cfg){ + var val = gridItem.config[cfg.key]; + if(val){ + cfg.value = stripModifier(val, cfg.modifier); + } + }); + } + + if(angular.isObject(gridItem.styles)){ + _.each(styles, function(style){ + var val = gridItem.styles[style.key]; + if(val){ + style.value = stripModifier(val, style.modifier); + } + }); + } + + $scope.gridItemSettingsDialog = {}; + $scope.gridItemSettingsDialog.view = "views/propertyeditors/grid/dialogs/config.html"; + $scope.gridItemSettingsDialog.title = "Settings"; + $scope.gridItemSettingsDialog.styles = styles; + $scope.gridItemSettingsDialog.config = config; + + $scope.gridItemSettingsDialog.show = true; + + $scope.gridItemSettingsDialog.submit = function(model) { + + var styleObject = {}; + var configObject = {}; + + _.each(model.styles, function(style){ + if(style.value){ + styleObject[style.key] = addModifier(style.value, style.modifier); + } + }); + _.each(model.config, function (cfg) { + if (cfg.value) { + configObject[cfg.key] = addModifier(cfg.value, cfg.modifier); } }); + gridItem.styles = styleObject; + gridItem.config = configObject; + gridItem.hasConfig = gridItemHasConfig(styleObject, configObject); + + currentForm.$setDirty(); + + $scope.gridItemSettingsDialog.show = false; + $scope.gridItemSettingsDialog = null; + }; + + $scope.gridItemSettingsDialog.close = function(oldModel) { + $scope.gridItemSettingsDialog.show = false; + $scope.gridItemSettingsDialog = null; + }; + }; + function stripModifier(val, modifier) { + if (!val || !modifier || modifier.indexOf(placeHolder) < 0) { + return val; + } else { + var paddArray = modifier.split(placeHolder); + if(paddArray.length == 1){ + if (modifier.indexOf(placeHolder) === 0) { + return val.slice(0, -paddArray[0].length); + } else { + return val.slice(paddArray[0].length, 0); + } + } else { + if (paddArray[1].length === 0) { + return val.slice(paddArray[0].length); + } + return val.slice(paddArray[0].length, -paddArray[1].length); + } + } + } + + var addModifier = function(val, modifier){ + if (!modifier || modifier.indexOf(placeHolder) < 0) { + return val; + } else { + return modifier.replace(placeHolder, val); + } + }; + + function gridItemHasConfig(styles, config) { + + if(_.isEmpty(styles) && _.isEmpty(config)) { + return false; + } else { + return true; + } + + } + // ********************************************* // Area management functions // ********************************************* - $scope.setCurrentCell = function (cell) { - $scope.currentCell = cell; + $scope.clickCell = function(index, cells, row) { + cells[index].active = true; + row.hasActiveChild = true; }; - $scope.disableCurrentCell = function () { - $scope.currentCell = null; + $scope.clickOutsideCell = function(index, cells, row) { + cells[index].active = false; + row.hasActiveChild = hasActiveChild(row, cells); }; $scope.cellPreview = function (cell) { @@ -328,49 +476,37 @@ angular.module("umbraco") } }; - $scope.setInfohighlightArea = function (cell) { - $scope.currentInfohighlightArea = cell; - }; - - $scope.disableInfohighlightArea = function () { - $scope.currentInfohighlightArea = null; - }; - // ********************************************* // Control management functions // ********************************************* - $scope.setCurrentControl = function (Control) { - $scope.currentControl = Control; + $scope.clickControl = function (index, controls, cell) { + controls[index].active = true; + cell.hasActiveChild = true; }; - $scope.disableCurrentControl = function (Control) { - $scope.currentControl = null; + $scope.clickOutsideControl = function (index, controls, cell) { + controls[index].active = false; + cell.hasActiveChild = hasActiveChild(cell, controls); }; - $scope.setCurrentToolsControl = function (Control) { - $scope.currentToolsControl = Control; - }; + function hasActiveChild(item, children) { - $scope.disableCurrentToolsControl = function (Control) { - $scope.currentToolsControl = null; - }; + var activeChild = false; - $scope.setWarnhighlightControl = function (Control) { - $scope.currentWarnhighlightControl = Control; - }; + for(var i = 0; children.length > i; i++) { + var child = children[i]; - $scope.disableWarnhighlightControl = function (Control) { - $scope.currentWarnhighlightControl = null; - }; + if(child.active) { + activeChild = true; + } + } - $scope.setInfohighlightControl = function (Control) { - $scope.currentInfohighlightControl = Control; - }; + if(activeChild) { + return true; + } - $scope.disableInfohighlightControl = function (Control) { - $scope.currentInfohighlightControl = null; - }; + } var guid = (function () { @@ -390,7 +526,7 @@ angular.module("umbraco") }; $scope.addControl = function (editor, cell, index, initialize) { - $scope.closeItemOverlay(); + initialize = (initialize !== false); var newControl = { @@ -403,10 +539,13 @@ angular.module("umbraco") index = cell.controls.length; } + newControl.active = true; + //populate control $scope.initControl(newControl, index + 1); - cell.controls.splice(index + 1, 0, newControl); + cell.controls.push(newControl); + }; $scope.addTinyMce = function (cell) { @@ -434,17 +573,25 @@ angular.module("umbraco") e.stopPropagation(); }; - $scope.showPrompt = function (scopedObject) { - scopedObject.deletePrompt = true; + $scope.togglePrompt = function (scopedObject) { + scopedObject.deletePrompt = !scopedObject.deletePrompt; + }; + + $scope.hidePrompt = function (scopedObject) { + scopedObject.deletePrompt = false; + }; + + $scope.toggleAddRow = function() { + $scope.showRowConfigurations = !$scope.showRowConfigurations; }; // ********************************************* - // INITIALISATION + // Initialization // these methods are called from ng-init on the template // so we can controll their first load data // - // intialisation sets non-saved data like percentage sizing, allowed editors and + // intialization sets non-saved data like percentage sizing, allowed editors and // other data that should all be pre-fixed with $ to strip it out on save // ********************************************* @@ -467,7 +614,7 @@ angular.module("umbraco") $scope.model.config.items.columns = 12; } - if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0) { + if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { @@ -539,6 +686,9 @@ angular.module("umbraco") } } }); + + // if there is more than one row added - hide row add tools + $scope.showRowConfigurations = false; } }; @@ -559,6 +709,8 @@ angular.module("umbraco") original = angular.copy(original); original.styles = row.styles; original.config = row.config; + original.hasConfig = gridItemHasConfig(row.styles, row.config); + //sync area configuration _.each(original.areas, function (area, areaIndex) { @@ -570,6 +722,7 @@ angular.module("umbraco") if (currentArea) { area.config = currentArea.config; area.styles = currentArea.styles; + area.hasConfig = gridItemHasConfig(currentArea.styles, currentArea.config); } //set editor permissions diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 05809cad02..339ee9d455 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -1,4 +1,20 @@ -
+
+ + + + + + + + + +
@@ -7,6 +23,8 @@
+

+
@@ -14,7 +32,6 @@
@@ -28,187 +45,194 @@
- {{layout.name}} + {{template.name}}
-

- -
+ -
+
-
-
+
-
+ + +
- -
+
- -
-
+
+
{{row.name}}
- - - - - - - +
+
-
- - - -
+ +
+ +
+ +
+ +
+ + + +
+ +
-
- - - -
-
+
- {{row.name}} -
-
+
- -
+ +
- -
+ +
+ + +
- -
-
+ +
+ + +
- - - - - +
+ +
- -
-
+
+
+ +
+
- -
- - - -
+
- -
- - - -
+ +
+
+ + +
+
-
+ +
-
- - {{control.editor.name}} +
- -
-
-
-
- - -
-
- -

-
+
+ {{control.editor.name}} +
-
+
- - - +
-
-
+
+ {{control.editor.name}} +
+ +
+ +
+ +
+ +
+ + + +
+ +
+ +
+ + +
+
+ +
+
+ +
+ + + +
+
+
+ +
+
@@ -222,7 +246,22 @@ -
+
+ + + + + + + +
+ +
+ +

-

- -

- -
@@ -261,4 +295,18 @@
-
\ No newline at end of file + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js index bfe5d1ba2e..810623320c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js @@ -74,25 +74,40 @@ angular.module("umbraco") template *****************/ - $scope.configureTemplate = function(template){ - if(template === undefined){ - template = { - name: "", - sections:[ + $scope.configureTemplate = function(template) { - ] - }; - $scope.model.value.templates.push(template); - } - - dialogService.open( - { - template: "views/propertyEditors/grid/dialogs/layoutconfig.html", - currentLayout: template, - rows: $scope.model.value.layouts, - columns: $scope.model.value.columns - } - ); + var templatesCopy = angular.copy($scope.model.value.templates); + + if (template === undefined) { + template = { + name: "", + sections: [ + + ] + }; + $scope.model.value.templates.push(template); + } + + $scope.layoutConfigOverlay = {}; + $scope.layoutConfigOverlay.view = "views/propertyEditors/grid/dialogs/layoutconfig.html"; + $scope.layoutConfigOverlay.currentLayout = template; + $scope.layoutConfigOverlay.rows = $scope.model.value.layouts; + $scope.layoutConfigOverlay.columns = $scope.model.value.columns; + $scope.layoutConfigOverlay.show = true; + + $scope.layoutConfigOverlay.submit = function(model) { + $scope.layoutConfigOverlay.show = false; + $scope.layoutConfigOverlay = null; + }; + + $scope.layoutConfigOverlay.close = function(oldModel) { + + //reset templates + $scope.model.value.templates = templatesCopy; + + $scope.layoutConfigOverlay.show = false; + $scope.layoutConfigOverlay = null; + } }; @@ -105,48 +120,64 @@ angular.module("umbraco") Row *****************/ - $scope.configureLayout = function(layout){ + $scope.configureLayout = function(layout) { - if(layout === undefined){ - layout = { - name: "", - areas:[ + var layoutsCopy = angular.copy($scope.model.value.layouts); - ] - }; - $scope.model.value.layouts.push(layout); - } + if(layout === undefined){ + layout = { + name: "", + areas:[ + + ] + }; + $scope.model.value.layouts.push(layout); + } + + $scope.rowConfigOverlay = {}; + $scope.rowConfigOverlay.view = "views/propertyEditors/grid/dialogs/rowconfig.html"; + $scope.rowConfigOverlay.currentRow = layout; + $scope.rowConfigOverlay.editors = $scope.editors; + $scope.rowConfigOverlay.columns = $scope.model.value.columns; + $scope.rowConfigOverlay.show = true; + + $scope.rowConfigOverlay.submit = function(model) { + $scope.rowConfigOverlay.show = false; + $scope.rowConfigOverlay = null; + }; + + $scope.rowConfigOverlay.close = function(oldModel) { + $scope.model.value.layouts = layoutsCopy; + $scope.rowConfigOverlay.show = false; + $scope.rowConfigOverlay = null; + }; - dialogService.open( - { - template: "views/propertyEditors/grid/dialogs/rowconfig.html", - currentRow: layout, - editors: $scope.editors, - columns: $scope.model.value.columns - } - ); }; //var rowDeletesPending = false; - $scope.deleteLayout = function (index) { - //rowDeletesPending = true; + $scope.deleteLayout = function(index) { + + $scope.rowDeleteOverlay = {}; + $scope.rowDeleteOverlay.view = "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html"; + $scope.rowDeleteOverlay.dialogData = { + rowName: $scope.model.value.layouts[index].name + }; + $scope.rowDeleteOverlay.show = true; + + $scope.rowDeleteOverlay.submit = function(model) { + + $scope.model.value.layouts.splice(index, 1); + + $scope.rowDeleteOverlay.show = false; + $scope.rowDeleteOverlay = null; + }; + + $scope.rowDeleteOverlay.close = function(oldModel) { + $scope.rowDeleteOverlay.show = false; + $scope.rowDeleteOverlay = null; + }; - //show ok/cancel dialog - var confirmDialog = dialogService.open( - { - template: "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html", - show: true, - callback: function() { - $scope.model.value.layouts.splice(index, 1); - }, - dialogData: { - rowName: $scope.model.value.layouts[index].name - } - } - ); - }; - /**************** @@ -176,30 +207,40 @@ angular.module("umbraco") collection.splice(index, 1); }; - var editConfigCollection = function(configValues, title, callbackOnSave){ - dialogService.open( - { - template: "views/propertyeditors/grid/dialogs/editconfig.html", - config: configValues, - name: title, - callback: function(data){ - callbackOnSave(data); - } - }); + var editConfigCollection = function(configValues, title, callback) { + + $scope.editConfigCollectionOverlay = {}; + $scope.editConfigCollectionOverlay.view = "views/propertyeditors/grid/dialogs/editconfig.html"; + $scope.editConfigCollectionOverlay.config = configValues; + $scope.editConfigCollectionOverlay.title = title; + $scope.editConfigCollectionOverlay.show = true; + + $scope.editConfigCollectionOverlay.submit = function(model) { + + callback(model.config) + + $scope.editConfigCollectionOverlay.show = false; + $scope.editConfigCollectionOverlay = null; + }; + + $scope.editConfigCollectionOverlay.close = function(oldModel) { + $scope.editConfigCollectionOverlay.show = false; + $scope.editConfigCollectionOverlay = null; + }; + }; - $scope.editConfig = function(){ - editConfigCollection($scope.model.value.config, "Settings", function(data){ - $scope.model.value.config = data; - }); - }; - - $scope.editStyles = function(){ - editConfigCollection($scope.model.value.styles, "Styling", function(data){ - $scope.model.value.styles = data; - }); + $scope.editConfig = function() { + editConfigCollection($scope.model.value.config, "Settings", function(data) { + $scope.model.value.config = data; + }); }; + $scope.editStyles = function() { + editConfigCollection($scope.model.value.styles, "Styling", function(data){ + $scope.model.value.styles = data; + }); + }; /**************** editors diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html index b7f46f0996..9d9626694a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.html @@ -1,5 +1,5 @@
-
+

@@ -32,7 +32,7 @@ ng-click="deleteTemplate($index)">
-
@@ -160,4 +160,33 @@
+ + + + + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 42eee9b4a8..1e8db1a24b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -2,9 +2,10 @@ //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco') .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager) { + function ($rootScope, $routeParams, $scope, $log, mediaHelper, cropperHelper, $timeout, editorState, umbRequestHelper, fileManager, angularHelper) { var config = angular.copy($scope.model.config); + $scope.imageIsLoaded = false; //move previously saved value to the editor if ($scope.model.value) { @@ -55,6 +56,10 @@ angular.module('umbraco') if ($scope.model.value) { delete $scope.model.value; } + + // set form to dirty to tricker discard changes dialog + var currForm = angularHelper.getCurrentForm($scope); + currForm.$setDirty(); }; //show previews @@ -67,6 +72,10 @@ angular.module('umbraco') } }; + $scope.imageLoaded = function() { + $scope.imageIsLoaded = true; + }; + //on image selected, update the cropper $scope.$on("filesSelected", function (ev, args) { $scope.model.value = config; @@ -112,7 +121,7 @@ angular.module('umbraco') if (property.value.src) { if (thumbnail === true) { - return property.value.src + "?width=500&mode=max"; + return property.value.src + "?width=500&mode=max&animationprocessmode=first"; } else { return property.value.src; @@ -145,4 +154,4 @@ angular.module('umbraco') return null; }); } - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 29148b32eb..e274249b42 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -16,10 +16,10 @@ \ No newline at end of file +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html index 69d54244e8..e8c0eaf63c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html @@ -6,7 +6,7 @@ ui-sortable ng-model="model.value">
  • - + {{node.alias}} @@ -24,26 +24,26 @@
    + ng-model="newItem.alias" val-highlight="{{hasError}}" />
    + ng-model="newItem.width" class="umb-editor-tiny" val-highlight="{{hasError}}" /> × + ng-model="newItem.height" class="umb-editor-tiny" val-highlight="{{hasError}}" />
    - - Cancel + + Cancel
  • - +
    -
    \ No newline at end of file +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html index 251a6066d8..2108340d5c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html @@ -1,10 +1,13 @@ 
    - + Not a number {{propertyForm.requiredField.errorMsg}} -
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/bulkActionPermissions.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/bulkActionPermissions.prevalues.html new file mode 100644 index 0000000000..8a5ca613e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/bulkActionPermissions.prevalues.html @@ -0,0 +1,32 @@ +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js new file mode 100644 index 0000000000..abe2b41bd1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.controller.js @@ -0,0 +1,88 @@ +/** + * @ngdoc controller + * @name Umbraco.PrevalueEditors.ListViewLayoutsPreValsController + * @function + * + * @description + * The controller for configuring layouts for list views + */ +(function() { + "use strict"; + + function ListViewLayoutsPreValsController($scope) { + + var vm = this; + vm.focusLayoutName = false; + + vm.layoutsSortableOptions = { + distance: 10, + tolerance: "pointer", + opacity: 0.7, + scroll: true, + cursor: "move", + handle: ".list-view-layout__sort-handle" + }; + + vm.addLayout = addLayout; + vm.showPrompt = showPrompt; + vm.hidePrompt = hidePrompt; + vm.removeLayout = removeLayout; + vm.openIconPicker = openIconPicker; + + function activate() { + + + + } + + function addLayout() { + + vm.focusLayoutName = false; + + var layout = { + "name": "", + "path": "", + "icon": "icon-stop", + "selected": true + }; + + $scope.model.value.push(layout); + + } + + function showPrompt(layout) { + layout.deletePrompt = true; + } + + function hidePrompt(layout) { + layout.deletePrompt = false; + } + + function removeLayout($index, layout) { + $scope.model.value.splice($index, 1); + } + + function openIconPicker(layout) { + vm.iconPickerDialog = { + view: "iconpicker", + show: true, + submit: function(model) { + if (model.color) { + layout.icon = model.icon + " " + model.color; + } else { + layout.icon = model.icon; + } + vm.focusLayoutName = true; + vm.iconPickerDialog.show = false; + vm.iconPickerDialog = null; + } + }; + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.PrevalueEditors.ListViewLayoutsPreValsController", ListViewLayoutsPreValsController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html new file mode 100644 index 0000000000..c71f3306cd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html @@ -0,0 +1,57 @@ +
    + +
    + +
    + + + +
    + + + + + +
    + +
    + +
    + +
    + + {{ layout.name }} + (system layout) +
    + +
    + +
    + +
    + +
    + + + +
    +
    + +
    + + Add layout + +
    + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html new file mode 100644 index 0000000000..a2c0ff534e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -0,0 +1,84 @@ +
    + +
    + + + + + + + + + + + + +
    + +
    + + + + + + + + + + + +
      +
    • +
      {{ property.header }}
      +
      {{ vm.mediaDetailsTooltip.item[property.alias] }}
      +
    • +
    +
    + + + + + + + + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js new file mode 100644 index 0000000000..24c00ebe1d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.listviewlayout.controller.js @@ -0,0 +1,114 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DocumentType.EditController + * @function + * + * @description + * The controller for the content type editor + */ +(function() { + "use strict"; + + function ListViewGridLayoutController($scope, $routeParams, mediaHelper, mediaResource, $location, listViewHelper) { + + var vm = this; + + vm.nodeId = $scope.contentId; + //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.mediaDetailsTooltip = {}; + vm.itemsWithoutFolders = []; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + + vm.hoverMediaItemDetails = hoverMediaItemDetails; + vm.selectContentItem = selectContentItem; + vm.selectItem = selectItem; + vm.selectFolder = selectFolder; + vm.goToItem = goToItem; + + function activate() { + vm.itemsWithoutFolders = filterOutFolders($scope.items); + } + + function filterOutFolders(items) { + + var newArray = []; + + if(items && items.length ) { + + for (var i = 0; items.length > i; i++) { + var item = items[i]; + var isFolder = !mediaHelper.hasFilePropertyType(item); + + if (!isFolder) { + newArray.push(item); + } + } + + } + + return newArray; + } + + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + + function hoverMediaItemDetails(item, event, hover) { + + if (hover && !vm.mediaDetailsTooltip.show) { + + vm.mediaDetailsTooltip.event = event; + vm.mediaDetailsTooltip.item = item; + vm.mediaDetailsTooltip.show = true; + + } else if (!hover && vm.mediaDetailsTooltip.show) { + + vm.mediaDetailsTooltip.show = false; + + } + + } + + function selectContentItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, $scope.items, $scope.selection, $event); + } + + function selectItem(item, $event, $index) { + listViewHelper.selectHandler(item, $index, vm.itemsWithoutFolders, $scope.selection, $event); + } + + function selectFolder(folder, $event, $index) { + listViewHelper.selectHandler(folder, $index, $scope.folders, $scope.selection, $event); + } + + function goToItem(item, $event, $index) { + $location.path($scope.entityType + '/' + $scope.entityType + '/edit/' + item.id); + } + + activate(); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.GridLayoutController", ListViewGridLayoutController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html new file mode 100644 index 0000000000..96bebb7abf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.html @@ -0,0 +1,66 @@ +
    + +
    + + + + + + + +
    + +
    + + + + +
    + + + + + + + + + +
    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 new file mode 100644 index 0000000000..660bd38ce7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -0,0 +1,75 @@ +(function () { + "use strict"; + + function ListViewListLayoutController($scope, listViewHelper, $location, mediaHelper) { + + var vm = this; + + vm.nodeId = $scope.contentId; + //we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.disallowedUploadFiles); + vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; + vm.activeDrag = false; + vm.isRecycleBin = $scope.contentId === '-21' || $scope.contentId === '-20'; + + vm.selectItem = selectItem; + vm.clickItem = clickItem; + vm.selectAll = selectAll; + vm.isSelectedAll = isSelectedAll; + vm.isSortDirection = isSortDirection; + vm.sort = sort; + vm.dragEnter = dragEnter; + vm.dragLeave = dragLeave; + vm.onFilesQueue = onFilesQueue; + vm.onUploadComplete = onUploadComplete; + + function selectAll($event) { + listViewHelper.selectAllItems($scope.items, $scope.selection, $event); + } + + function isSelectedAll() { + return listViewHelper.isSelectedAll($scope.items, $scope.selection); + } + + function selectItem(selectedItem, $index, $event) { + listViewHelper.selectHandler(selectedItem, $index, $scope.items, $scope.selection, $event); + } + + function clickItem(item) { + $location.path($scope.entityType + '/' +$scope.entityType + '/edit/' +item.id); + } + + function isSortDirection(col, direction) { + return listViewHelper.setSortingDirection(col, direction, $scope.options); + } + + function sort(field, allow, isSystem) { + if (allow) { + $scope.options.orderBySystemField = isSystem; + listViewHelper.setSorting(field, allow, $scope.options); + $scope.getContent($scope.contentId); + } + } + + // Dropzone upload functions + function dragEnter(el, event) { + vm.activeDrag = true; + } + + function dragLeave(el, event) { + vm.activeDrag = false; + } + + function onFilesQueue() { + vm.activeDrag = false; + } + + function onUploadComplete() { + $scope.getContent($scope.contentId); + } + + } + +angular.module("umbraco").controller("Umbraco.PropertyEditors.ListView.ListLayoutController", ListViewListLayoutController); + +}) (); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 0569c9faca..594475a7d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,442 +1,617 @@ -function listViewController($rootScope, $scope, $routeParams, $injector, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q) { +function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService, navigationService, treeService) { - //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content - // that isn't created yet, if we continue this will use the parent id in the route params which isn't what - // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove - // the list view tab entirely when it's new. - if ($routeParams.create) { - $scope.isNew = true; - return; - } + //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content + // that isn't created yet, if we continue this will use the parent id in the route params which isn't what + // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove + // the list view tab entirely when it's new. + if ($routeParams.create) { + $scope.isNew = true; + return; + } - //Now we need to check if this is for media, members or content because that will depend on the resources we use - var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; - - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { - $scope.entityType = "member"; - contentResource = $injector.get('memberResource'); - getContentTypesCallback = $injector.get('memberTypeResource').getTypes; - getListResultsCallback = contentResource.getPagedResults; - deleteItemCallback = contentResource.deleteByKey; - getIdCallback = function(selected) { - return selected.key; - }; - createEditUrlCallback = function(item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; - }; - } - else { - //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) - if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { - $scope.entityType = "media"; - contentResource = $injector.get('mediaResource'); - getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; - } - else { - $scope.entityType = "content"; - contentResource = $injector.get('contentResource'); - getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; - } - getListResultsCallback = contentResource.getChildren; - deleteItemCallback = contentResource.deleteById; - getIdCallback = function(selected) { - return selected.id; - }; - createEditUrlCallback = function(item) { - return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; - }; - } + //Now we need to check if this is for media, members or content because that will depend on the resources we use + var contentResource, getContentTypesCallback, getListResultsCallback, deleteItemCallback, getIdCallback, createEditUrlCallback; - $scope.pagination = []; - $scope.isNew = false; - $scope.actionInProgress = false; - $scope.listViewResultSet = { - totalPages: 0, - items: [] - }; + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "member") || (appState.getSectionState("currentSection") === "member")) { + $scope.entityType = "member"; + contentResource = $injector.get('memberResource'); + getContentTypesCallback = $injector.get('memberTypeResource').getTypes; + getListResultsCallback = contentResource.getPagedResults; + deleteItemCallback = contentResource.deleteByKey; + getIdCallback = function (selected) { + var selectedKey = getItemKey(selected.id); + return selectedKey; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.key + "?page=" + $scope.options.pageNumber + "&listName=" + $scope.contentId; + }; + } + else { + //check the config for the entity type, or the current section name (since the config is only set in c#, not in pre-vals) + if (($scope.model.config.entityType && $scope.model.config.entityType === "media") || (appState.getSectionState("currentSection") === "media")) { + $scope.entityType = "media"; + contentResource = $injector.get('mediaResource'); + getContentTypesCallback = $injector.get('mediaTypeResource').getAllowedTypes; + } + else { + $scope.entityType = "content"; + contentResource = $injector.get('contentResource'); + getContentTypesCallback = $injector.get('contentTypeResource').getAllowedTypes; + } + getListResultsCallback = contentResource.getChildren; + deleteItemCallback = contentResource.deleteById; + getIdCallback = function (selected) { + return selected.id; + }; + createEditUrlCallback = function (item) { + return "/" + $scope.entityType + "/" + $scope.entityType + "/edit/" + item.id + "?page=" + $scope.options.pageNumber; + }; + } - $scope.options = { - pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, - pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, - filter: '', - orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), - orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", - includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ - { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, - { alias: 'updater', header: 'Last edited by', isSystem: 1 } - ], - allowBulkPublish: true, - allowBulkUnpublish: true, - allowBulkDelete: true, - }; + $scope.pagination = []; + $scope.isNew = false; + $scope.actionInProgress = false; + $scope.selection = []; + $scope.folders = []; + $scope.listViewResultSet = { + totalPages: 0, + items: [] + }; - //update all of the system includeProperties to enable sorting - _.each($scope.options.includeProperties, function(e, i) { - - if (e.isSystem) { + $scope.currentNodePermissions = {} - //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted - // to do that, we'd need to update the base query for content to include the content type alias column - // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? - if (e.alias != "contentTypeAlias") { - e.allowSorting = true; - } - - //localize the header - var key = getLocalizedKey(e.alias); - localizationService.localize(key).then(function (v) { - e.header = v; + //Just ensure we do have an editorState + if (editorState.current) { + //Fetch current node allowed actions for the current user + //This is the current node & not each individual child node in the list + var currentUserPermissions = editorState.current.allowedActions; + + //Create a nicer model rather than the funky & hard to remember permissions strings + $scope.currentNodePermissions = { + "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O + "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C + "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D + "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M + "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U + "canUnpublish": _.contains(currentUserPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) + }; + } + + //when this is null, we don't check permissions + $scope.buttonPermissions = null; + + //When we are dealing with 'content', we need to deal with permissions on child nodes. + // Currently there is no real good way to + if ($scope.entityType === "content") { + + var idsWithPermissions = null; + + $scope.buttonPermissions = { + canCopy: true, + canCreate: true, + canDelete: true, + canMove: true, + canPublish: true, + canUnpublish: true + }; + + $scope.$watch(function () { + return $scope.selection.length; + }, function (newVal, oldVal) { + + if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) { + + //get all of the selected ids + var ids = _.map($scope.selection, function (i) { + return i.id.toString(); }); - } - }); - function showNotificationsAndReset(err, reload, successMsg) { + //remove the dictionary items that don't have matching ids + var filtered = {}; + _.each(idsWithPermissions, function (value, key, list) { + if (_.contains(ids, key)) { + filtered[key] = value; + } + }); + idsWithPermissions = filtered; - //check if response is ysod - if(err.status && err.status >= 500) { - dialogService.ysodDialog(err); - } + //find all ids that we haven't looked up permissions for + var existingIds = _.keys(idsWithPermissions); + var missingLookup = _.map(_.difference(ids, existingIds), function (i) { + return Number(i); + }); - $timeout(function() { - $scope.bulkStatus = ""; - $scope.actionInProgress = false; - }, 500); - - if (reload === true) { - $scope.reloadView($scope.contentId); - } - - if (err.data && angular.isArray(err.data.notifications)) { - for (var i = 0; i < err.data.notifications.length; i++) { - notificationsService.showNotification(err.data.notifications[i]); - } - } - else if (successMsg) { - notificationsService.success("Done", successMsg); - } - } - - $scope.isSortDirection = function (col, direction) { - return $scope.options.orderBy.toUpperCase() == col.toUpperCase() && $scope.options.orderDirection == direction; - } - - $scope.next = function() { - if ($scope.options.pageNumber < $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber++; - $scope.reloadView($scope.contentId); - //TODO: this would be nice but causes the whole view to reload - //$location.search("page", $scope.options.pageNumber); - } - }; - - $scope.goToPage = function(pageNumber) { - $scope.options.pageNumber = pageNumber + 1; - $scope.reloadView($scope.contentId); - //TODO: this would be nice but causes the whole view to reload - //$location.search("page", $scope.options.pageNumber); - }; - - $scope.sort = function(field, allow) { - if (allow) { - $scope.options.orderBy = field; - - if ($scope.options.orderDirection === "desc") { - $scope.options.orderDirection = "asc"; + if (missingLookup.length > 0) { + contentResource.getPermissions(missingLookup).then(function (p) { + $scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions); + }); } else { - $scope.options.orderDirection = "desc"; + $scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions); } + } + }); - $scope.reloadView($scope.contentId); + } + + $scope.options = { + displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1, + pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10, + pageNumber: ($routeParams.page && Number($routeParams.page) != NaN && Number($routeParams.page) > 0) ? $routeParams.page : 1, + filter: '', + orderBy: ($scope.model.config.orderBy ? $scope.model.config.orderBy : 'VersionDate').trim(), + orderDirection: $scope.model.config.orderDirection ? $scope.model.config.orderDirection.trim() : "desc", + orderBySystemField: true, + includeProperties: $scope.model.config.includeProperties ? $scope.model.config.includeProperties : [ + { alias: 'updateDate', header: 'Last edited', isSystem: 1 }, + { alias: 'updater', header: 'Last edited by', isSystem: 1 } + ], + layout: { + layouts: $scope.model.config.layouts, + activeLayout: listViewHelper.getLayout($routeParams.id, $scope.model.config.layouts) + }, + orderBySystemField: true, + allowBulkPublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkPublish, + allowBulkUnpublish: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkUnpublish, + allowBulkCopy: $scope.entityType === 'content' && $scope.model.config.bulkActionPermissions.allowBulkCopy, + allowBulkMove: $scope.model.config.bulkActionPermissions.allowBulkMove, + allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete + }; + + //update all of the system includeProperties to enable sorting + _.each($scope.options.includeProperties, function (e, i) { + + //NOTE: special case for contentTypeAlias, it's a system property that cannot be sorted + // to do that, we'd need to update the base query for content to include the content type alias column + // which requires another join and would be slower. BUT We are doing this for members so not sure it makes a diff? + if (e.alias != "contentTypeAlias") { + e.allowSorting = true; } - }; - $scope.prev = function() { - if ($scope.options.pageNumber > 1) { - $scope.options.pageNumber--; - $scope.reloadView($scope.contentId); - //TODO: this would be nice but causes the whole view to reload - //$location.search("page", $scope.options.pageNumber); + // Another special case for lasted edited data/update date for media, again this field isn't available on the base table so we can't sort by it + if (e.isSystem && $scope.entityType == "media") { + e.allowSorting = e.alias != 'updateDate'; } - }; - - /*Loads the search results, based on parameters set in prev,next,sort and so on*/ - /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state - with simple values */ + // Another special case for members, only fields on the base table (cmsMember) can be used for sorting + if (e.isSystem && $scope.entityType == "member") { + e.allowSorting = e.alias == 'username' || e.alias == 'email'; + } - $scope.reloadView = function (id) { + if (e.isSystem) { + //localize the header + var key = getLocalizedKey(e.alias); + localizationService.localize(key).then(function (v) { + e.header = v; + }); + } + }); - getListResultsCallback(id, $scope.options).then(function (data) { + $scope.selectLayout = function (selectedLayout) { + $scope.options.layout.activeLayout = listViewHelper.setLayout($routeParams.id, selectedLayout, $scope.model.config.layouts); + }; - $scope.actionInProgress = false; + function showNotificationsAndReset(err, reload, successMsg) { - $scope.listViewResultSet = data; + //check if response is ysod + if (err.status && err.status >= 500) { - //update all values for display - if ($scope.listViewResultSet.items) { - _.each($scope.listViewResultSet.items, function (e, index) { - setPropertyValues(e); - }); - } + // Open ysod overlay + $scope.ysodOverlay = { + view: "ysod", + error: err, + show: true + }; + } - //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example - // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last - // available page and then re-load again - if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber = $scope.listViewResultSet.totalPages; + $timeout(function () { + $scope.bulkStatus = ""; + $scope.actionInProgress = false; + }, 500); - //reload! - $scope.reloadView(id); - } + if (reload === true) { + $scope.reloadView($scope.contentId); + } - $scope.pagination = []; + if (err.data && angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + else if (successMsg) { + notificationsService.success("Done", successMsg); + } + } - //list 10 pages as per normal - if ($scope.listViewResultSet.totalPages <= 10) { - for (var i = 0; i < $scope.listViewResultSet.totalPages; i++) { - $scope.pagination.push({ - val: (i + 1), - isActive: $scope.options.pageNumber == (i + 1) + $scope.next = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + $scope.goToPage = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + $scope.prev = function (pageNumber) { + $scope.options.pageNumber = pageNumber; + $scope.reloadView($scope.contentId); + }; + + + /*Loads the search results, based on parameters set in prev,next,sort and so on*/ + /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state + with simple values */ + + $scope.reloadView = function (id) { + + $scope.viewLoaded = false; + + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + + getListResultsCallback(id, $scope.options).then(function (data) { + + $scope.actionInProgress = false; + $scope.listViewResultSet = data; + + //update all values for display + if ($scope.listViewResultSet.items) { + _.each($scope.listViewResultSet.items, function (e, index) { + setPropertyValues(e); + }); + } + + if ($scope.entityType === 'media') { + + mediaResource.getChildFolders($scope.contentId) + .then(function (folders) { + $scope.folders = folders; + $scope.viewLoaded = true; }); - } - } - else { - //if there is more than 10 pages, we need to do some fancy bits - //get the max index to start - var maxIndex = $scope.listViewResultSet.totalPages - 10; - //set the start, but it can't be below zero - var start = Math.max($scope.options.pageNumber - 5, 0); - //ensure that it's not too far either - start = Math.min(maxIndex, start); + } else { + $scope.viewLoaded = true; + } - for (var i = start; i < (10 + start) ; i++) { - $scope.pagination.push({ - val: (i + 1), - isActive: $scope.options.pageNumber == (i + 1) - }); - } + //NOTE: This might occur if we are requesting a higher page number than what is actually available, for example + // if you have more than one page and you delete all items on the last page. In this case, we need to reset to the last + // available page and then re-load again + if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber = $scope.listViewResultSet.totalPages; - //now, if the start is greater than 0 then '1' will not be displayed, so do the elipses thing - if (start > 0) { - $scope.pagination.unshift({ name: "First", val: 1, isActive: false }, {val: "...",isActive: false}); - } + //reload! + $scope.reloadView(id); + } - //same for the end - if (start < maxIndex) { - $scope.pagination.push({ val: "...", isActive: false }, { name: "Last", val: $scope.listViewResultSet.totalPages, isActive: false }); - } - } + }); + }; - }); - }; + var searchListView = _.debounce(function () { + $scope.$apply(function () { + makeSearch(); + }); + }, 500); - $scope.$watch(function() { - return $scope.options.filter; - }, _.debounce(function(newVal, oldVal) { - $scope.$apply(function() { - if (newVal !== null && newVal !== undefined && newVal !== oldVal) { - $scope.options.pageNumber = 1; - $scope.actionInProgress = true; - $scope.reloadView($scope.contentId); - } - }); - }, 200)); + $scope.forceSearch = function (ev) { + //13: enter + switch (ev.keyCode) { + case 13: + makeSearch(); + break; + } + }; - $scope.enterSearch = function($event) { - $($event.target).next().focus(); - } + $scope.enterSearch = function () { + $scope.viewLoaded = false; + searchListView(); + }; - $scope.selectAll = function($event) { - var checkbox = $event.target; - if (!angular.isArray($scope.listViewResultSet.items)) { - return; - } - for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { - var entity = $scope.listViewResultSet.items[i]; - entity.selected = checkbox.checked; - } - }; + function makeSearch() { + if ($scope.options.filter !== null && $scope.options.filter !== undefined) { + $scope.options.pageNumber = 1; + //$scope.actionInProgress = true; + $scope.reloadView($scope.contentId); + } + } - $scope.isSelectedAll = function() { - if (!angular.isArray($scope.listViewResultSet.items)) { - return false; - } - return _.every($scope.listViewResultSet.items, function(item) { - return item.selected; - }); - }; + $scope.isAnythingSelected = function () { + if ($scope.selection.length === 0) { + return false; + } else { + return true; + } + }; - $scope.isAnythingSelected = function() { - if (!angular.isArray($scope.listViewResultSet.items)) { - return false; - } - return _.some($scope.listViewResultSet.items, function(item) { - return item.selected; - }); - }; + $scope.selectedItemsCount = function () { + return $scope.selection.length; + }; - $scope.getIcon = function(entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; + $scope.clearSelection = function () { + listViewHelper.clearSelection($scope.listViewResultSet.items, $scope.folders, $scope.selection); + }; - function serial(selected, fn, getStatusMsg, index) { - return fn(selected, index).then(function (content) { - index++; - $scope.bulkStatus = getStatusMsg(index, selected.length); - return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; - }, function (err) { - var reload = index > 0; - showNotificationsAndReset(err, reload); - return err; - }); - } + $scope.getIcon = function (entry) { + return iconHelper.convertFromLegacyIcon(entry.icon); + }; - function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { - var selected = _.filter($scope.listViewResultSet.items, function (item) { - return item.selected; - }); - if (selected.length === 0) - return; - if (confirmMsg && !confirm(confirmMsg)) - return; + function serial(selected, fn, getStatusMsg, index) { + return fn(selected, index).then(function (content) { + index++; + $scope.bulkStatus = getStatusMsg(index, selected.length); + return index < selected.length ? serial(selected, fn, getStatusMsg, index) : content; + }, function (err) { + var reload = index > 0; + showNotificationsAndReset(err, reload); + return err; + }); + } - $scope.actionInProgress = true; - $scope.bulkStatus = getStatusMsg(0, selected.length); + function applySelected(fn, getStatusMsg, getSuccessMsg, confirmMsg) { + var selected = $scope.selection; + if (selected.length === 0) + return; + if (confirmMsg && !confirm(confirmMsg)) + return; - serial(selected, fn, getStatusMsg, 0).then(function (result) { - // executes once the whole selection has been processed - // in case of an error (caught by serial), result will be the error - if (!(result.data && angular.isArray(result.data.notifications))) - showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); - }); - }; + $scope.actionInProgress = true; + $scope.bulkStatus = getStatusMsg(0, selected.length); - $scope.delete = function () { - applySelected( - function (selected, index) { return deleteItemCallback(getIdCallback(selected[index])) }, - function (count, total) { return "Deleted " + count + " out of " + total + " document" + (total > 1 ? "s" : "") }, - function (total) { return "Deleted " + total + " document" + (total > 1 ? "s" : "") }, - "Sure you want to delete?"); - }; + return serial(selected, fn, getStatusMsg, 0).then(function (result) { + // executes once the whole selection has been processed + // in case of an error (caught by serial), result will be the error + if (!(result.data && angular.isArray(result.data.notifications))) + showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); + }); + } - $scope.publish = function () { - applySelected( - function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, - function (count, total) { return "Published " + count + " out of " + total + " document" + (total > 1 ? "s" : "") }, - function (total) { return "Published " + total + " document" + (total > 1 ? "s" : "") }); - }; + $scope.delete = function () { - $scope.unpublish = function() { - applySelected( - function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, - function (count, total) { return "Unpublished " + count + " out of " + total + " document" + (total > 1 ? "s" : "") }, - function (total) { return "Unpublished " + total + " document" + (total > 1 ? "s" : "") }); - }; + var attempt = + applySelected( + function(selected, index) { return deleteItemCallback(getIdCallback(selected[index])); }, + function(count, total) { + return "Deleted " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); + }, + function(total) { return "Deleted " + total + " item" + (total > 1 ? "s" : ""); }, + "Sure you want to delete?"); + if (attempt) { + attempt.then(function () { + //executes if all is successful, let's sync the tree + var activeNode = appState.getTreeState("selectedNode"); + if (activeNode) { + navigationService.reloadNode(activeNode); + } + }); + } + }; - function getCustomPropertyValue(alias, properties) { - var value = ''; - var index = 0; - var foundAlias = false; - for (var i = 0; i < properties.length; i++) { - if (properties[i].alias == alias) { - foundAlias = true; - break; - } - index++; - } + $scope.publish = function () { + applySelected( + function (selected, index) { return contentResource.publishById(getIdCallback(selected[index])); }, + function (count, total) { return "Published " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Published " + total + " item" + (total > 1 ? "s" : ""); }); + }; - if (foundAlias) { - value = properties[index].value; - } + $scope.unpublish = function () { + applySelected( + function (selected, index) { return contentResource.unPublish(getIdCallback(selected[index])); }, + function (count, total) { return "Unpublished " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Unpublished " + total + " item" + (total > 1 ? "s" : ""); }); + }; - return value; - }; + $scope.move = function () { + $scope.moveDialog = {}; + $scope.moveDialog.title = "Move"; + $scope.moveDialog.section = $scope.entityType; + $scope.moveDialog.currentNode = $scope.contentId; + $scope.moveDialog.view = "move"; + $scope.moveDialog.show = true; - /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ - function setPropertyValues(result) { + $scope.moveDialog.submit = function (model) { - //set the edit url - result.editPath = createEditUrlCallback(result); + if (model.target) { + performMove(model.target); + } - _.each($scope.options.includeProperties, function (e, i) { + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; - var alias = e.alias; + $scope.moveDialog.close = function (oldModel) { + $scope.moveDialog.show = false; + $scope.moveDialog = null; + }; - // First try to pull the value directly from the alias (e.g. updatedBy) - var value = result[alias]; - - // If this returns an object, look for the name property of that (e.g. owner.name) - if (value === Object(value)) { - value = value['name']; - } - - // If we've got nothing yet, look at a user defined property - if (typeof value === 'undefined') { - value = getCustomPropertyValue(alias, result.properties); - } - - // If we have a date, format it - if (isDate(value)) { - value = value.substring(0, value.length - 3); - } - - // set what we've got on the result - result[alias] = value; - }); + }; - }; + function performMove(target) { - function isDate(val) { - if (angular.isString(val)) { - return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); - } - return false; - }; + //NOTE: With the way this applySelected/serial works, I'm not sure there's a better way currently to return + // a specific value from one of the methods, so we'll have to try this way. Even though the first method + // will fire once per every node moved, the destination path will be the same and we need to use that to sync. + var newPath = null; + applySelected( + function(selected, index) { + return contentResource.move({ parentId: target.id, id: getIdCallback(selected[index]) }).then(function(path) { + newPath = path; + return path; + }); + }, + function(count, total) {return "Moved " + count + " out of " + total + " item" + (total > 1 ? "s" : "");}, + function(total) { return "Moved " + total + " item" + (total > 1 ? "s" : ""); }) + .then(function() { + //executes if all is successful, let's sync the tree + if (newPath) { - function initView() { - if ($routeParams.id) { - $scope.listViewAllowedTypes = getContentTypesCallback($routeParams.id); + //we need to do a double sync here: first refresh the node where the content was moved, + // then refresh the node where the content was moved from + navigationService.syncTree({ tree: target.nodeType, path: newPath, forceReload: true, activate: false }).then(function (args) { + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + if (activeNode) { + navigationService.reloadNode(activeNode); + } + }); + } + }); + } - $scope.contentId = $routeParams.id; - $scope.isTrashed = $routeParams.id === "-20" || $routeParams.id === "-21"; + $scope.copy = function () { + $scope.copyDialog = {}; + $scope.copyDialog.title = "Copy"; + $scope.copyDialog.section = $scope.entityType; + $scope.copyDialog.currentNode = $scope.contentId; + $scope.copyDialog.view = "copy"; + $scope.copyDialog.show = true; - $scope.reloadView($scope.contentId); - } - }; + $scope.copyDialog.submit = function (model) { + if (model.target) { + performCopy(model.target, model.relateToOriginal); + } - function getLocalizedKey(alias) { + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; - switch (alias) { - case "sortOrder": - return "general_sort"; - case "updateDate": - return "content_updateDate"; - case "updater": - return "content_updatedBy"; - case "createDate": - return "content_createDate"; - case "owner": - return "content_createBy"; - case "published": - return "content_isPublished"; - case "contentTypeAlias": - //TODO: Check for members - return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; - case "email": - return "general_email"; - case "username": - return "general_username"; - } - return alias; - } + $scope.copyDialog.close = function (oldModel) { + $scope.copyDialog.show = false; + $scope.copyDialog = null; + }; - //GO! - initView(); + }; + + function performCopy(target, relateToOriginal) { + applySelected( + function (selected, index) { return contentResource.copy({ parentId: target.id, id: getIdCallback(selected[index]), relateToOriginal: relateToOriginal }); }, + function (count, total) { return "Copied " + count + " out of " + total + " item" + (total > 1 ? "s" : ""); }, + function (total) { return "Copied " + total + " item" + (total > 1 ? "s" : ""); }); + } + + function getCustomPropertyValue(alias, properties) { + var value = ''; + var index = 0; + var foundAlias = false; + for (var i = 0; i < properties.length; i++) { + if (properties[i].alias == alias) { + foundAlias = true; + break; + } + index++; + } + + if (foundAlias) { + value = properties[index].value; + } + + return value; + } + + /** This ensures that the correct value is set for each item in a row, we don't want to call a function during interpolation or ng-bind as performance is really bad that way */ + function setPropertyValues(result) { + + //set the edit url + result.editPath = createEditUrlCallback(result); + + _.each($scope.options.includeProperties, function (e, i) { + + var alias = e.alias; + + // First try to pull the value directly from the alias (e.g. updatedBy) + var value = result[alias]; + + // If this returns an object, look for the name property of that (e.g. owner.name) + if (value === Object(value)) { + value = value['name']; + } + + // If we've got nothing yet, look at a user defined property + if (typeof value === 'undefined') { + value = getCustomPropertyValue(alias, result.properties); + } + + // If we have a date, format it + if (isDate(value)) { + value = value.substring(0, value.length - 3); + } + + // set what we've got on the result + result[alias] = value; + }); + + + } + + function isDate(val) { + if (angular.isString(val)) { + return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); + } + return false; + } + + function initView() { + //default to root id if the id is undefined + var id = $routeParams.id; + if (id === undefined) { + id = -1; + } + + $scope.listViewAllowedTypes = getContentTypesCallback(id); + + $scope.contentId = id; + $scope.isTrashed = id === "-20" || id === "-21"; + + $scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed; + $scope.options.allowBulkUnpublish = $scope.options.allowBulkUnpublish && !$scope.isTrashed; + + $scope.options.bulkActionsAllowed = $scope.options.allowBulkPublish || + $scope.options.allowBulkUnpublish || + $scope.options.allowBulkCopy || + $scope.options.allowBulkMove || + $scope.options.allowBulkDelete; + + $scope.reloadView($scope.contentId); + } + + function getLocalizedKey(alias) { + + switch (alias) { + case "sortOrder": + return "general_sort"; + case "updateDate": + return "content_updateDate"; + case "updater": + return "content_updatedBy"; + case "createDate": + return "content_createDate"; + case "owner": + return "content_createBy"; + case "published": + return "content_isPublished"; + case "contentTypeAlias": + //TODO: Check for members + return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; + case "email": + return "general_email"; + case "username": + return "general_username"; + } + return alias; + } + + function getItemKey(itemId) { + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var item = $scope.listViewResultSet.items[i]; + if (item.id === itemId) { + return item.key; + } + } + } + + //GO! + initView(); } -angular.module("umbraco").controller("Umbraco.PropertyEditors.ListViewController", listViewController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.PropertyEditors.ListViewController", listViewController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 211524caf0..742d1251cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -1,139 +1,197 @@
    -
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + {{ selectedItemsCount() }} of {{ listViewResultSet.items.length }} selected + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    -
    -
    + + - + + - - - + + -
    -
    -
    - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - Name - - - - - - - -
    -

    There are no items show in the list.

    -
    - - - - - - {{result[column.alias]}} -
    -
    -
    - -
    -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js index 14cf1592e6..b74e30004e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js @@ -43,21 +43,33 @@ angular.module('umbraco') var macro = $scope.renderModel[index]; dialogData["macroData"] = macro; } - - dialogService.macroPicker({ - dialogData : dialogData, - callback: function(data) { - collectDetails(data); + $scope.macroPickerOverlay = {}; + $scope.macroPickerOverlay.view = "macropicker"; + $scope.macroPickerOverlay.dialogData = dialogData; + $scope.macroPickerOverlay.show = true; - //update the raw syntax and the list... - if(index !== null && $scope.renderModel[index]) { - $scope.renderModel[index] = data; - } else { - $scope.renderModel.push(data); - } + $scope.macroPickerOverlay.submit = function(model) { + + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + collectDetails(macroObject); + + //update the raw syntax and the list... + if(index !== null && $scope.renderModel[index]) { + $scope.renderModel[index] = macroObject; + } else { + $scope.renderModel.push(macroObject); } - }); + + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + + $scope.macroPickerOverlay.close = function(oldModel) { + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + }; + } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html index af3b82a257..96c74c9c55 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html @@ -25,4 +25,12 @@ -
    \ No newline at end of file + + + + +
    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 8893ebd523..7b3d5698c5 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 @@ -1,5 +1,5 @@ //inject umbracos assetsServce and dialog service -function MarkdownEditorController($scope, assetsService, dialogService, $timeout) { +function MarkdownEditorController($scope, $element, assetsService, dialogService, angularHelper, $timeout) { //tell the assets service to load the markdown.editor libs from the markdown editors //plugin folder @@ -8,6 +8,29 @@ function MarkdownEditorController($scope, assetsService, dialogService, $timeout $scope.model.value = $scope.model.config.defaultValue; } + function openMediaPicker(callback) { + + $scope.mediaPickerOverlay = {}; + $scope.mediaPickerOverlay.view = "mediaPicker"; + $scope.mediaPickerOverlay.show = true; + $scope.mediaPickerOverlay.disableFolderSelect = true; + + $scope.mediaPickerOverlay.submit = function(model) { + + var selectedImagePath = model.selectedImages[0].image; + callback(selectedImagePath); + + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + + $scope.mediaPickerOverlay.close = function(model) { + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + }; + + } + assetsService .load([ "lib/markdown/markdown.converter.js", @@ -19,7 +42,7 @@ function MarkdownEditorController($scope, assetsService, dialogService, $timeout // we need a short delay to wait for the textbox to appear. setTimeout(function () { //this function will execute when all dependencies have loaded - // but in the case that they've been previously loaded, we can only + // but in the case that they've been previously loaded, we can only // init the md editor after this digest because the DOM needs to be ready first // so run the init on a timeout $timeout(function () { @@ -29,15 +52,16 @@ function MarkdownEditorController($scope, assetsService, dialogService, $timeout //subscribe to the image dialog clicks editor2.hooks.set("insertImageDialog", function (callback) { - - dialogService.mediaPicker({ - callback: function (data) { - callback(data.image); - } - }); - + openMediaPicker(callback); return true; // tell the editor that we'll take care of getting the image url }); + + editor2.hooks.set("onPreviewRefresh", function () { + angularHelper.getCurrentForm($scope).$setDirty(); + // We must manually update the model as there is no way to hook into the markdown editor events without editing that code. + $scope.model.value = $("textarea", $element).val(); + }); + }, 200); }); @@ -46,4 +70,4 @@ function MarkdownEditorController($scope, assetsService, dialogService, $timeout }) } -angular.module("umbraco").controller("Umbraco.PropertyEditors.MarkdownEditorController", MarkdownEditorController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.PropertyEditors.MarkdownEditorController", MarkdownEditorController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html index 482a0ae1cc..64ea1e8e2b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html @@ -1,7 +1,15 @@ -
    -
    - - - -
    -
    \ No newline at end of file +
    +
    + + + +
    + + + + +
    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 d2342ec5bc..bbf61a32d3 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,22 +1,22 @@ //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) { + function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location) { //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; + var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; + var disableFolderSelect = $scope.model.config.disableFolderSelect && $scope.model.config.disableFolderSelect !== '0' ? true : false; if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { $scope.model.config.startNodeId = userData.startMediaId; }); } - - function setupViewModel() { $scope.images = []; - $scope.ids = []; + $scope.ids = []; if ($scope.model.value) { var ids = $scope.model.value.split(','); @@ -31,16 +31,16 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl entityResource.getByIds(ids, "Media").then(function (medias) { _.each(medias, function (media, i) { - + //only show non-trashed items if (media.parentId >= -1) { - if (!media.thumbnail) { + if (!media.thumbnail) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } $scope.images.push(media); - $scope.ids.push(media.id); + $scope.ids.push(media.id); } }); @@ -57,37 +57,47 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.sync(); }; - $scope.add = function() { - dialogService.mediaPicker({ - startNodeId: $scope.model.config.startNodeId, - multiPicker: multiPicker, - callback: function(data) { - - //it's only a single selector, so make it into an array - if (!multiPicker) { - data = [data]; - } - - _.each(data, function(media, i) { - - if (!media.thumbnail) { - media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); - } - - $scope.images.push(media); - $scope.ids.push(media.id); - }); - - $scope.sync(); - } - }); + $scope.goToItem = function(item) { + $location.path('media/media/edit/' + item.id); }; + $scope.add = function() { + + $scope.mediaPickerOverlay = { + view: "mediapicker", + title: "Select media", + startNodeId: $scope.model.config.startNodeId, + multiPicker: multiPicker, + onlyImages: onlyImages, + disableFolderSelect: disableFolderSelect, + show: true, + submit: function(model) { + + _.each(model.selectedImages, function(media, i) { + + if (!media.thumbnail) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } + + $scope.images.push(media); + $scope.ids.push(media.id); + }); + + $scope.sync(); + + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + + } + }; + + }; + $scope.sortableOptions = { 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){ 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 9b74e9267f..096685f1a9 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 @@ -2,23 +2,40 @@
    • - - - - {{image.name}} - + + + + + {{image.name}} + + + -
    -
    \ No newline at end of file + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js index 1c801cddb3..5ebbd37217 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js @@ -15,38 +15,38 @@ function memberGroupPicker($scope, dialogService){ $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); }); } - - var dialogOptions = { - multiPicker: true, - entityType: "MemberGroup", - section: "membergroup", - treeAlias: "memberGroup", - filter: "", - filterCssClass: "not-allowed", - callback: function (data) { - if (angular.isArray(data)) { - _.each(data, function (item, i) { - $scope.add(item); - }); - } else { - $scope.clear(); - $scope.add(data); - } - } + $scope.openMemberGroupPicker = function() { + + $scope.memberGroupPicker = {}; + $scope.memberGroupPicker.multiPicker = true; + $scope.memberGroupPicker.view = "memberGroupPicker"; + $scope.memberGroupPicker.show = true; + + $scope.memberGroupPicker.submit = function(model) { + + if(model.selectedMemberGroups) { + _.each(model.selectedMemberGroups, function (item, i) { + $scope.add(item); + }); + } + + if(model.selectedMemberGroup) { + $scope.clear(); + $scope.add(model.selectedMemberGroup); + } + + $scope.memberGroupPicker.show = false; + $scope.memberGroupPicker = null; + }; + + $scope.memberGroupPicker.close = function(oldModel) { + $scope.memberGroupPicker.show = false; + $scope.memberGroupPicker = null; + }; + }; - //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the - // pre-value config on to the dialog options - if($scope.model.config){ - angular.extend(dialogOptions, $scope.model.config); - } - - $scope.openMemberGroupPicker =function() { - var d = dialogService.memberGroupPicker(dialogOptions); - }; - - $scope.remove =function(index){ $scope.renderModel.splice(index, 1); }; @@ -79,4 +79,4 @@ function memberGroupPicker($scope, dialogService){ } -angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberGroupPickerController", memberGroupPicker); \ No newline at end of file +angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberGroupPickerController", memberGroupPicker); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html index 316ad168f3..5258968c00 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html @@ -1,25 +1,33 @@
    - -
    \ No newline at end of file + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js index 7d5ad22fb8..9f20e121d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js @@ -1,6 +1,6 @@ //this controller simply tells the dialogs service to open a memberPicker window //with a specified callback, this callback will receive an object with a selection on it -function memberPickerController($scope, dialogService, entityResource, $log, iconHelper){ +function memberPickerController($scope, dialogService, entityResource, $log, iconHelper, angularHelper){ function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -27,19 +27,39 @@ function memberPickerController($scope, dialogService, entityResource, $log, ico $scope.clear(); $scope.add(data); } + angularHelper.getCurrentForm($scope).$setDirty(); } }; - //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the + //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options if ($scope.model.config) { angular.extend(dialogOptions, $scope.model.config); } - - $scope.openMemberPicker =function() { - var d = dialogService.memberPicker(dialogOptions); - }; + $scope.openMemberPicker = function() { + $scope.memberPickerOverlay = dialogOptions; + $scope.memberPickerOverlay.view = "memberPicker"; + $scope.memberPickerOverlay.show = true; + + $scope.memberPickerOverlay.submit = function(model) { + + if (model.selection) { + _.each(model.selection, function(item, i) { + $scope.add(item); + }); + } + + $scope.memberPickerOverlay.show = false; + $scope.memberPickerOverlay = null; + }; + + $scope.memberPickerOverlay.close = function(oldModel) { + $scope.memberPickerOverlay.show = false; + $scope.memberPickerOverlay = null; + }; + + }; $scope.remove =function(index){ $scope.renderModel.splice(index, 1); @@ -52,14 +72,14 @@ function memberPickerController($scope, dialogService, entityResource, $log, ico if (currIds.indexOf(item.id) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); - } + $scope.renderModel.push({name: item.name, id: item.id, icon: item.icon}); + } }; $scope.clear = function() { $scope.renderModel = []; }; - + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { var currIds = _.map($scope.renderModel, function (i) { return i.id; @@ -83,4 +103,4 @@ function memberPickerController($scope, dialogService, entityResource, $log, ico } -angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberPickerController", memberPickerController); \ No newline at end of file +angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberPickerController", memberPickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html index a223828046..b537a80113 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html @@ -1,25 +1,33 @@
    - -
    \ No newline at end of file + + + + +
    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 6abd80cbcd..cb6c8e595a 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 @@ -1,31 +1,72 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RelatedLinksController", - function ($rootScope, $scope, dialogService) { + function ($rootScope, $scope, dialogService, iconHelper) { if (!$scope.model.value) { $scope.model.value = []; } + + $scope.model.config.max = isNumeric($scope.model.config.max) && $scope.model.config.max !== 0 ? $scope.model.config.max : Number.MAX_VALUE; $scope.newCaption = ''; $scope.newLink = 'http://'; $scope.newNewWindow = false; $scope.newInternal = null; $scope.newInternalName = ''; + $scope.newInternalIcon = null; $scope.addExternal = true; $scope.currentEditLink = null; $scope.hasError = false; - $scope.internal = function ($event) { - $scope.currentEditLink = null; - var d = dialogService.contentPicker({ multipicker: false, callback: select }); - $event.preventDefault(); - }; - - $scope.selectInternal = function ($event, link) { + $scope.internal = function($event) { + + $scope.currentEditLink = null; + + $scope.contentPickerOverlay = {}; + $scope.contentPickerOverlay.view = "contentpicker"; + $scope.contentPickerOverlay.multiPicker = false; + $scope.contentPickerOverlay.show = true; + + $scope.contentPickerOverlay.submit = function(model) { + + select(model.selection[0]); + + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + + $scope.contentPickerOverlay.close = function(oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + + $event.preventDefault(); + }; + + $scope.selectInternal = function($event, link) { + + $scope.currentEditLink = link; + + $scope.contentPickerOverlay = {}; + $scope.contentPickerOverlay.view = "contentpicker"; + $scope.contentPickerOverlay.multiPicker = false; + $scope.contentPickerOverlay.show = true; + + $scope.contentPickerOverlay.submit = function(model) { + + select(model.selection[0]); + + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + + $scope.contentPickerOverlay.close = function(oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + }; + + $event.preventDefault(); - $scope.currentEditLink = link; - var d = dialogService.contentPicker({ multipicker: false, callback: select }); - $event.preventDefault(); }; $scope.edit = function (idx) { @@ -34,7 +75,6 @@ } $scope.model.value[idx].edit = true; }; - $scope.saveEdit = function (idx) { $scope.model.value[idx].title = $scope.model.value[idx].caption; @@ -69,6 +109,7 @@ this.edit = false; this.isInternal = true; this.internalName = $scope.newInternalName; + this.internalIcon = $scope.newInternalIcon; this.type = "internal"; this.title = $scope.newCaption; }; @@ -79,7 +120,7 @@ $scope.newNewWindow = false; $scope.newInternal = null; $scope.newInternalName = ''; - + $scope.newInternalIcon = null; } $event.preventDefault(); }; @@ -103,17 +144,32 @@ $scope.model.value[index + direction] = temp; }; + //helper for determining if a user can add items + $scope.canAdd = function () { + return $scope.model.config.max <= 0 || $scope.model.config.max > countVisible(); + } + + //helper that returns if an item can be sorted + $scope.canSort = function () { + return countVisible() > 1; + } + $scope.sortableOptions = { - containment: 'parent', + axis: 'y', + handle: '.handle', cursor: 'move', + cancel: '.no-drag', + containment: 'parent', + placeholder: 'sortable-placeholder', + forcePlaceholderSize: true, helper: function (e, ui) { - // When sorting , the cells collapse. This helper fixes that: http://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ + // When sorting table rows, the cells collapse. This helper fixes that: http://www.foliotek.com/devblog/make-table-rows-sortable-using-jquery-ui-sortable/ ui.children().each(function () { $(this).width($(this).width()); }); return ui; }, - items: '> tr', + items: '> tr:not(.unsortable)', tolerance: 'pointer', update: function (e, ui) { // Get the new and old index for the moved element (using the URL as the identifier) @@ -125,9 +181,36 @@ var movedElement = $scope.model.value[originalIndex]; $scope.model.value.splice(originalIndex, 1); $scope.model.value.splice(newIndex, 0, movedElement); + }, + start: function (e, ui) { + //ui.placeholder.html(""); + + // Build a placeholder cell that spans all the cells in the row: http://stackoverflow.com/questions/25845310/jquery-ui-sortable-and-table-cell-size + var cellCount = 0; + $('td, th', ui.helper).each(function () { + // For each td or th try and get it's colspan attribute, and add that or 1 to the total + var colspan = 1; + var colspanAttr = $(this).attr('colspan'); + if (colspanAttr > 1) { + colspan = colspanAttr; + } + cellCount += colspan; + }); + + // Add the placeholder UI - note that this is the item's content, so td rather than tr - and set height of tr + ui.placeholder.html('').height(ui.item.height()); } }; + //helper to count what is visible + function countVisible() { + return $scope.model.value.length; + } + + function isNumeric(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + function getElementIndexByUrl(url) { for (var i = 0; i < $scope.model.value.length; i++) { if ($scope.model.value[i].link == url) { @@ -142,10 +225,12 @@ if ($scope.currentEditLink != null) { $scope.currentEditLink.internal = data.id; $scope.currentEditLink.internalName = data.name; + $scope.currentEditLink.internalIcon = iconHelper.convertFromLegacyIcon(data.icon); $scope.currentEditLink.link = data.id; } else { $scope.newInternal = data.id; $scope.newInternalName = data.name; + $scope.newInternalIcon = iconHelper.convertFromLegacyIcon(data.icon); } - } - }); \ No newline at end of file + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html index 2f32f3875c..c6e02976f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html @@ -1,5 +1,5 @@  \ No newline at end of file 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 45812ceef2..bbb8024a53 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 @@ -1,7 +1,7 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RTEController", - function ($rootScope, $scope, $q, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource) { - + function ($rootScope, $scope, $q, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource, macroService) { + $scope.isLoading = true; //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias @@ -34,7 +34,7 @@ angular.module("umbraco") //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce - var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style]"; + var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],span[id|class|style]"; var invalidElements = tinyMceConfig.inValidElements; var plugins = _.map(tinyMceConfig.plugins, function (plugin) { @@ -64,8 +64,7 @@ angular.module("umbraco") //queue rules loading angular.forEach(editorConfig.stylesheets, function (val, key) { - stylesheets.push("../css/" + val + ".css?" + new Date().getTime()); - + stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css?" + new Date().getTime()); await.push(stylesheetResource.getRulesByName(val).then(function (rules) { angular.forEach(rules, function (rule) { var r = {}; @@ -118,7 +117,7 @@ angular.module("umbraco") width: editorConfig.dimensions.width, maxImageSize: editorConfig.maxImageSize, toolbar: toolbar, - content_css: stylesheets.join(','), + content_css: stylesheets, relative_urls: false, style_formats: styleFormats }; @@ -126,7 +125,7 @@ angular.module("umbraco") if (tinyMceConfig.customConfig) { - //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to + //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to // convert it to json instead of having it as a string since this is what tinymce requires for (var i in tinyMceConfig.customConfig) { var val = tinyMceConfig.customConfig[i]; @@ -135,7 +134,7 @@ angular.module("umbraco") if (val.detectIsJson()) { try { tinyMceConfig.customConfig[i] = JSON.parse(val); - //now we need to check if this custom config key is defined in our baseline, if it is we don't want to + //now we need to check if this custom config key is defined in our baseline, if it is we don't want to //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise //if it's an object it will overwrite the baseline if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { @@ -145,7 +144,7 @@ angular.module("umbraco") } catch (e) { //cannot parse, we'll just leave it - } + } } } } @@ -165,13 +164,13 @@ angular.module("umbraco") editor.getBody().setAttribute('spellcheck', true); }); - //We need to listen on multiple things here because of the nature of tinymce, it doesn't + //We need to listen on multiple things here because of the nature of tinymce, it doesn't //fire events when you think! //The change event doesn't fire when content changes, only when cursor points are changed and undo points - //are created. the blur event doesn't fire if you insert content into the editor with a button and then - //press save. - //We have a couple of options, one is to do a set timeout and check for isDirty on the editor, or we can - //listen to both change and blur and also on our own 'saving' event. I think this will be best because a + //are created. the blur event doesn't fire if you insert content into the editor with a button and then + //press save. + //We have a couple of options, one is to do a set timeout and check for isDirty on the editor, or we can + //listen to both change and blur and also on our own 'saving' event. I think this will be best because a //timer might end up using unwanted cpu and we'd still have to listen to our saving event in case they clicked //save before the timeout elapsed. @@ -191,7 +190,7 @@ angular.module("umbraco") // currForm.$setDirty(); // alreadyDirty = true; // } - + // }); //}); @@ -226,19 +225,75 @@ angular.module("umbraco") var srcAttr = $(e.target).attr("src"); var path = srcAttr.split("?")[0]; $(e.target).attr("data-mce-src", path + qs); - + syncContent(editor); }); + tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { + $scope.linkPickerOverlay = { + view: "linkpicker", + currentTarget: currentTarget, + show: true, + submit: function(model) { + tinyMceService.insertLinkInEditor(editor, model.target, anchorElement); + $scope.linkPickerOverlay.show = false; + $scope.linkPickerOverlay = null; + } + }; + }); //Create the insert media plugin - tinyMceService.createMediaPicker(editor, $scope); + tinyMceService.createMediaPicker(editor, $scope, function(currentTarget, userData){ + $scope.mediaPickerOverlay = { + currentTarget: currentTarget, + onlyImages: true, + showDetails: true, + disableFolderSelect: true, + startNodeId: userData.startMediaId, + view: "mediapicker", + show: true, + submit: function(model) { + tinyMceService.insertMediaInEditor(editor, model.selectedImages[0]); + $scope.mediaPickerOverlay.show = false; + $scope.mediaPickerOverlay = null; + } + }; + + }); + //Create the embedded plugin - tinyMceService.createInsertEmbeddedMedia(editor, $scope); + tinyMceService.createInsertEmbeddedMedia(editor, $scope, function() { + + $scope.embedOverlay = { + view: "embed", + show: true, + submit: function(model) { + tinyMceService.insertEmbeddedMediaInEditor(editor, model.embed.preview); + $scope.embedOverlay.show = false; + $scope.embedOverlay = null; + } + }; + + }); + //Create the insert macro plugin - tinyMceService.createInsertMacro(editor, $scope); + tinyMceService.createInsertMacro(editor, $scope, function(dialogData) { + + $scope.macroPickerOverlay = { + view: "macropicker", + dialogData: dialogData, + show: true, + submit: function(model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); + tinyMceService.insertMacroInEditor(editor, macroObject, $scope); + $scope.macroPickerOverlay.show = false; + $scope.macroPickerOverlay = null; + } + }; + + }); }; @@ -281,7 +336,7 @@ angular.module("umbraco") }); //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom + // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom // element might still be there even after the modal has been hidden. $scope.$on('$destroy', function () { unsubscribe(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html index cb808726ba..656f5b7490 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html @@ -3,4 +3,33 @@ -
    \ No newline at end of file + + + + + + + + + + + + + +
    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 c815bc0c5e..702d19a509 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 @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", - function ($scope, $timeout, $log, tinyMceService, stylesheetResource) { + function ($scope, $timeout, $log, tinyMceService, stylesheetResource, assetsService) { var cfg = tinyMceService.defaultPrevalues(); if($scope.model.value){ @@ -23,6 +23,14 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", tinyMceService.configuration().then(function(config){ $scope.tinyMceConfig = config; + // extend commands with properties for font-icon and if it is a custom command + $scope.tinyMceConfig.commands = _.map($scope.tinyMceConfig.commands, function (obj) { + var icon = getFontIcon(obj.frontEndCommand); + return angular.extend(obj, { + fontIcon: icon.name, + isCustom: icon.isCustom + }); + }); }); stylesheetResource.getAll().then(function(stylesheets){ @@ -57,6 +65,43 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", $scope.model.value.stylesheets.splice(index, 1); } }; + + // map properties for specific commands + function getFontIcon(alias) { + var icon = { name: alias, isCustom: false }; + + switch (alias) { + case "codemirror": + icon.name = "code"; + icon.isCustom = false; + break; + case "styleselect": + icon.name = "icon-list"; + icon.isCustom = true; + break; + case "umbembeddialog": + icon.name = "icon-tv"; + icon.isCustom = true; + break; + case "umbmediapicker": + icon.name = "icon-picture"; + icon.isCustom = true; + break; + case "umbmacro": + icon.name = "icon-settings-alt"; + icon.isCustom = true; + break; + case "umbmacro": + icon.name = "icon-settings-alt"; + icon.isCustom = true; + break; + default: + icon.name = alias; + icon.isCustom = false; + } + + return icon; + } var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { @@ -65,9 +110,11 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", }); - //when the scope is destroyed we need to unsubscribe + // when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { unsubscribe(); }); - }); + // load TinyMCE skin which contains css for font-icons + assetsService.loadCss("lib/tinymce/skins/umbraco/skin.min.css"); + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index f00dfa06c1..b9d63df9f3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -1,36 +1,43 @@ -
    +
    - +
    - - {{css.name}} +
    - - × - Pixels - + +
    + × + Pixels +
    +
    - Pixels +
    + Pixels +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/handle.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/handle.prevalues.html new file mode 100644 index 0000000000..ae5deb099c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/handle.prevalues.html @@ -0,0 +1,11 @@ +
    + + + + Required + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html index ee2958d195..b79bbc6c59 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/orientation.prevalues.html @@ -3,6 +3,8 @@ - - Required + + + Required +
    \ No newline at end of file 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 6aae47dcaf..76d1599508 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 @@ -4,6 +4,13 @@ if (!$scope.model.config.orientation) { $scope.model.config.orientation = "horizontal"; } + if (!$scope.model.config.enableRange) { + $scope.model.config.enableRange = false; + } + else { + $scope.model.config.enableRange = $scope.model.config.enableRange === "1" ? true : false; + } + if (!$scope.model.config.initVal1) { $scope.model.config.initVal1 = 0; } @@ -34,6 +41,76 @@ else { $scope.model.config.step = parseFloat($scope.model.config.step); } + + if (!$scope.model.config.handle) { + $scope.model.config.handle = "round"; + } + + if (!$scope.model.config.reversed) { + $scope.model.config.reversed = false; + } + else { + $scope.model.config.reversed = $scope.model.config.reversed === "1" ? true : false; + } + + if (!$scope.model.config.tooltip) { + $scope.model.config.tooltip = "show"; + } + + if (!$scope.model.config.tooltipSplit) { + $scope.model.config.tooltipSplit = false; + } + else { + $scope.model.config.tooltipSplit = $scope.model.config.tooltipSplit === "1" ? true : false; + } + + if ($scope.model.config.tooltipFormat) { + $scope.model.config.formatter = function (value) { + if (angular.isArray(value) && $scope.model.config.enableRange) { + return $scope.model.config.tooltipFormat.replace("{0}", value[0]).replace("{1}", value[1]); + } else { + return $scope.model.config.tooltipFormat.replace("{0}", value); + } + } + } + + if (!$scope.model.config.ticks) { + $scope.model.config.ticks = []; + } + else { + // returns comma-separated string to an array, e.g. [0, 100, 200, 300, 400] + $scope.model.config.ticks = _.map($scope.model.config.ticks.split(','), function (item) { + return parseInt(item.trim()); + }); + } + + if (!$scope.model.config.ticksPositions) { + $scope.model.config.ticksPositions = []; + } + else { + // returns comma-separated string to an array, e.g. [0, 30, 60, 70, 90, 100] + $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) { + $scope.model.config.ticksLabels = []; + } + else { + // returns comma-separated string to an array, e.g. ['$0', '$100', '$200', '$300', '$400'] + $scope.model.config.ticksLabels = _.map($scope.model.config.ticksLabels.split(','), function (item) { + return item.trim(); + }); + } + + if (!$scope.model.config.ticksSnapBounds) { + $scope.model.config.ticksSnapBounds = 0; + } + else { + $scope.model.config.ticksSnapBounds = parseFloat($scope.model.config.ticksSnapBounds); + } /** This creates the slider with the model values - it's called on startup and if the model value changes */ function createSlider() { @@ -42,9 +119,10 @@ var sliderVal = null; //configure the model value based on if range is enabled or not - if ($scope.model.config.enableRange === "1") { + if ($scope.model.config.enableRange == true) { //If no value saved yet - then use default value - if (!$scope.model.value) { + //If it contains a single value - then also create a new array value + if (!$scope.model.value || $scope.model.value.indexOf(",") == -1) { var i1 = parseFloat($scope.model.config.initVal1); var i2 = parseFloat($scope.model.config.initVal2); sliderVal = [ @@ -75,18 +153,30 @@ } //initiate slider, add event handler and get the instance reference (stored in data) - var slider = $element.find('.slider-item').slider({ + var slider = $element.find('.slider-item').bootstrapSlider({ max: $scope.model.config.maxVal, min: $scope.model.config.minVal, orientation: $scope.model.config.orientation, - selection: "after", + selection: $scope.model.config.reversed ? "after" : "before", step: $scope.model.config.step, - tooltip: "show", + precision: $scope.model.config.precision, + tooltip: $scope.model.config.tooltip, + tooltip_split: $scope.model.config.tooltipSplit, + tooltip_position: $scope.model.config.tooltipPosition, + handle: $scope.model.config.handle, + reversed: $scope.model.config.reversed, + ticks: $scope.model.config.ticks, + ticks_positions: $scope.model.config.ticksPositions, + ticks_labels: $scope.model.config.ticksLabels, + ticks_snap_bounds: $scope.model.config.ticksSnapBounds, + formatter: $scope.model.config.formatter, + range: $scope.model.config.enableRange, //set the slider val - we cannot do this with data- attributes when using ranges value: sliderVal - }).on('slideStop', function () { + }).on('slideStop', function (e) { + var value = e.value; angularHelper.safeApply($scope, function () { - setModelValueFromSlider(slider.getValue()); + setModelValueFromSlider(value); }); }).data('slider'); } @@ -95,7 +185,7 @@ the model with the currently selected slider value(s) **/ function setModelValueFromSlider(sliderVal) { //Get the value from the slider and format it correctly, if it is a range we want a comma delimited value - if ($scope.model.config.enableRange === "1") { + if ($scope.model.config.enableRange == true) { $scope.model.value = sliderVal.join(","); } else { @@ -122,7 +212,7 @@ }); //load the separate css for the editor to avoid it blocking our js loading - assetsService.loadCss("lib/slider/slider.css"); - + 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 diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/tooltip.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/tooltip.prevalues.html new file mode 100644 index 0000000000..9baf195eb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/tooltip.prevalues.html @@ -0,0 +1,11 @@ +
    + + + + Required + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index dd41a439a3..a1e48bbc99 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -20,7 +20,9 @@ angular.module("umbraco") $scope.model.value = []; } else { - $scope.model.value = $scope.model.value.split(","); + if($scope.model.value.length > 0) { + $scope.model.value = $scope.model.value.split(","); + } } } } @@ -188,4 +190,4 @@ angular.module("umbraco") }); } -); \ No newline at end of file +); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html index b1049a2309..e9fbde9027 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html @@ -14,6 +14,7 @@ + +Required \ No newline at end of file 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 b2c57b4737..c55fc350fa 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,7 +4,6 @@ val-server="value" ng-required="model.validation.mandatory" ng-trim="false" /> - - Required + Required
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js index 65b3aed589..2637d23f69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js @@ -1,13 +1,29 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.UrlListController", function($rootScope, $scope, $filter) { - function formatDisplayValue() { - $scope.renderModel = _.map($scope.model.value.split(","), function (item) { - return { - url: item, - urlTarget: ($scope.config && $scope.config.target) ? $scope.config.target : "_blank" - }; - }); + function formatDisplayValue() { + if (angular.isArray($scope.model.value)) { + //it's the json value + $scope.renderModel = _.map($scope.model.value, function (item) { + return { + url: item.url, + linkText: item.linkText, + urlTarget: (item.target) ? item.target : "_blank", + icon: (item.icon) ? item.icon : "icon-out" + }; + }); + } + else { + //it's the default csv value + $scope.renderModel = _.map($scope.model.value.split(","), function (item) { + return { + url: item, + linkText: "", + urlTarget: ($scope.config && $scope.config.target) ? $scope.config.target : "_blank", + icon: ($scope.config && $scope.config.icon) ? $scope.config.icon : "icon-out" + }; + }); + } } $scope.getUrl = function(valueUrl) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html index f26f4dd9c1..f154a2a7b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html @@ -1,7 +1,11 @@  \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/test/config/app.unit.js b/src/Umbraco.Web.UI.Client/test/config/app.unit.js index b1d192c495..0f6a57d369 100644 --- a/src/Umbraco.Web.UI.Client/test/config/app.unit.js +++ b/src/Umbraco.Web.UI.Client/test/config/app.unit.js @@ -5,7 +5,8 @@ var app = angular.module('umbraco', [ 'umbraco.services', 'umbraco.mocks', 'umbraco.security', - 'ngCookies' + 'ngCookies', + 'LocalStorageModule' ]); /* For Angular 1.2: we need to load in Routing separately diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index c6b7d31bcb..0f323f03aa 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -26,7 +26,7 @@ module.exports = function(karma) { 'lib/../build/belle/lib/underscore/underscore-min.js', 'lib/umbraco/Extensions.js', 'lib/../build/belle/lib/rgrove-lazyload/lazyload.js', - + 'lib/../build/belle/lib/angular-local-storage/angular-local-storage.min.js', 'test/config/app.unit.js', 'src/common/mocks/umbraco.servervariables.js', diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js index 0fc5a4cedd..0d9b8a29e2 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js @@ -5,7 +5,7 @@ describe('edit content controller tests', function () { beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils) { + beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils, localizationMocks) { //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -17,6 +17,7 @@ describe('edit content controller tests', function () { //see /mocks/content.mocks.js for how its setup contentMocks.register(); entityMocks.register(); + localizationMocks.register(); //this controller requires an angular form controller applied to it scope.contentForm = angularHelper.getNullForm("contentForm"); 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 0d2b1a7bd9..35179c5646 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 @@ -5,7 +5,7 @@ describe('edit media controller tests', function () { beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, mediaMocks, entityMocks, mocksUtils) { + beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, mediaMocks, entityMocks, mocksUtils, localizationMocks) { //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -16,6 +16,7 @@ describe('edit media controller tests', function () { //see /mocks/content.mocks.js for how its setup mediaMocks.register(); entityMocks.register(); + localizationMocks.register(); //this controller requires an angular form controller applied to it scope.contentForm = angularHelper.getNullForm("contentForm"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js index 3444f39407..e6d1312109 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js @@ -5,7 +5,7 @@ describe('Content picker controller tests', function () { beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, entityMocks, mocksUtils) { + beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, entityMocks, mocksUtils, localizationMocks) { //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -28,6 +28,7 @@ describe('Content picker controller tests', function () { //have the contentMocks register its expect urls on the httpbackend //see /mocks/content.mocks.js for how its setup entityMocks.register(); + localizationMocks.register(); controller = $controller('Umbraco.PropertyEditors.ContentPickerController', { $scope: scope, diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js index 0fa55c2a11..ad336cf544 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js @@ -8,7 +8,9 @@ describe('contentEditingHelper tests', function () { //Only for 1.2: beforeEach(module('ngRoute')); - beforeEach(inject(function ($injector) { + beforeEach(inject(function ($injector, localizationMocks) { + localizationMocks.register(); + contentEditingHelper = $injector.get('contentEditingHelper'); $routeParams = $injector.get('$routeParams'); serverValidationManager = $injector.get('serverValidationManager'); @@ -108,7 +110,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - formHelper.handleServerValidation({ "Property.bodyText": ["Required"] }); + formHelper.handleServerValidation({ "_Properties.bodyText": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -124,7 +126,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - formHelper.handleServerValidation({ "Property.bodyText.value": ["Required"] }); + formHelper.handleServerValidation({ "_Properties.bodyText.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -144,8 +146,8 @@ describe('contentEditingHelper tests', function () { { "Name": ["Required"], "UpdateDate": ["Invalid date"], - "Property.bodyText.value": ["Required field"], - "Property.textarea": ["Invalid format"] + "_Properties.bodyText.value": ["Required field"], + "_Properties.textarea": ["Invalid format"] }); //assert diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/list-view-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/list-view-helper.spec.js new file mode 100644 index 0000000000..8726da7c14 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/list-view-helper.spec.js @@ -0,0 +1,48 @@ +describe('list view helper tests', function () { + var $scope, listViewHelper; + + beforeEach(module('LocalStorageModule')); + beforeEach(module('umbraco.services')); + + beforeEach(inject(function ($injector) { + $scope = $injector.get('$rootScope'); + listViewHelper = $injector.get('listViewHelper'); + })); + + describe('getButtonPermissions', function () { + + it('should update the currentIdsWithPermissions dictionary', function () { + + var currentIdsWithPermissions = {}; + var result = listViewHelper.getButtonPermissions({ "1234": ["A", "B", "C"] }, currentIdsWithPermissions); + + expect(_.has(currentIdsWithPermissions, "1234")).toBe(true); + expect(_.keys(currentIdsWithPermissions).length).toEqual(1); + + }); + + it('returns button permissions', function () { + + var currentIdsWithPermissions = {}; + var result1 = listViewHelper.getButtonPermissions({ "1234": ["O", "C", "D", "M", "U"] }, currentIdsWithPermissions); + + expect(result1["canCopy"]).toBe(true); + expect(result1["canCreate"]).toBe(true); + expect(result1["canDelete"]).toBe(true); + expect(result1["canMove"]).toBe(true); + expect(result1["canPublish"]).toBe(true); + expect(result1["canUnpublish"]).toBe(true); + + var result2 = listViewHelper.getButtonPermissions({ "1234": ["A", "B"] }, currentIdsWithPermissions); + + expect(result2["canCopy"]).toBe(false); + expect(result2["canCreate"]).toBe(false); + expect(result2["canDelete"]).toBe(false); + expect(result2["canMove"]).toBe(false); + expect(result2["canPublish"]).toBe(false); + expect(result2["canUnpublish"]).toBe(false); + + }); + + }); +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js new file mode 100644 index 0000000000..942b79eabd --- /dev/null +++ b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.controller.js @@ -0,0 +1,31 @@ +function modelsBuilderController($scope, umbRequestHelper, $log, $http, modelsBuilderResource) { + + $scope.generate = function() { + $scope.generating = true; + umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")), + 'Failed to generate.') + .then(function (result) { + $scope.generating = false; + $scope.dashboard = result; + }); + }; + + $scope.reload = function () { + $scope.ready = false; + modelsBuilderResource.getDashboard().then(function (result) { + $scope.dashboard = result; + $scope.ready = true; + }); + }; + + function init() { + modelsBuilderResource.getDashboard().then(function(result) { + $scope.dashboard = result; + $scope.ready = true; + }); + } + + init(); +} +angular.module("umbraco").controller("Umbraco.Dashboard.ModelsBuilderController", modelsBuilderController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.htm b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.htm new file mode 100644 index 0000000000..5049e5f157 --- /dev/null +++ b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.htm @@ -0,0 +1,41 @@ +
    + +
    + +
    + +

    Models Builder

    + +
    + Loading... +
    + +
    +
    + +
    +

    Models are out-of-date. +

    +
    + +
    +
    +

    Generating models will restart the application.

    +
    +
    + +
    +
    +
    +
    +
    + +
    + Last generation failed with the following error: +
    {{dashboard.lastError}}
    +
    +
    + +
    diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.resource.js b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.resource.js new file mode 100644 index 0000000000..58ca77cbdb --- /dev/null +++ b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/modelsbuilder.resource.js @@ -0,0 +1,23 @@ +function modelsBuilderResource($q, $http, umbRequestHelper) { + + return { + getModelsOutOfDateStatus: function () { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "GetModelsOutOfDateStatus")), + "Failed to get models out-of-date status"); + }, + + buildModels: function () { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "BuildModels")), + "Failed to build models"); + }, + + getDashboard: function () { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("modelsBuilderBaseUrl", "GetDashboard")), + "Failed to get dashboard"); + } + }; +} +angular.module("umbraco.resources").factory("modelsBuilderResource", modelsBuilderResource); diff --git a/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest new file mode 100644 index 0000000000..d83523517a --- /dev/null +++ b/src/Umbraco.Web.UI/App_Plugins/ModelsBuilder/package.manifest @@ -0,0 +1,7 @@ +{ + //array of files we want to inject into the application on app_start + javascript: [ + '~/App_Plugins/ModelsBuilder/modelsbuilder.controller.js', + '~/App_Plugins/ModelsBuilder/modelsbuilder.resource.js' + ] +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 39258f6b1a..4bd0be1308 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -40,7 +40,8 @@ v4.5 true - + + @@ -135,12 +136,12 @@ False ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - ..\packages\ImageProcessor.2.2.8.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.3.3.0\lib\net45\ImageProcessor.dll True - - ..\packages\ImageProcessor.Web.4.3.6.0\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.5.3.0\lib\net45\ImageProcessor.Web.dll True @@ -159,6 +160,14 @@ False ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + ..\packages\Microsoft.CodeAnalysis.Common.1.0.0\lib\net45\Microsoft.CodeAnalysis.dll + True + + + ..\packages\Microsoft.CodeAnalysis.CSharp.1.0.0\lib\net45\Microsoft.CodeAnalysis.CSharp.dll + True + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll @@ -190,7 +199,7 @@ False - ..\packages\MySql.Data.6.9.7\lib\net45\MySql.Data.dll + ..\packages\MySql.Data.6.9.8\lib\net45\MySql.Data.dll ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll @@ -203,20 +212,22 @@ System + + ..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + System.Data - - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.dll - False + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.dll True - - ..\packages\SqlServerCE.4.0.0.0\lib\System.Data.SqlServerCe.Entity.dll - False + + ..\packages\SqlServerCE.4.0.0.1\lib\System.Data.SqlServerCe.Entity.dll True @@ -235,6 +246,10 @@ False False + + ..\packages\System.Reflection.Metadata.1.0.21\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + @@ -333,8 +348,13 @@ umbraco.providers - - ..\packages\UrlRewritingNet.UrlRewriter.2.0.60829.1\lib\UrlRewritingNet.UrlRewriter.dll + + ..\packages\Umbraco.ModelsBuilder.3.0.2\lib\Umbraco.ModelsBuilder.dll + True + + + ..\packages\UrlRewritingNet.UrlRewriter.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll + True @@ -362,13 +382,6 @@ True Settings.settings - - ContentTypeControlNew.ascx - ASPXCodeBehind - - - ContentTypeControlNew.ascx - ImageViewer.ascx ASPXCodeBehind @@ -529,13 +542,6 @@ QuickSearch.ascx - - EditNodeTypeNew.aspx - ASPXCodeBehind - - - EditNodeTypeNew.aspx - ASPXCodeBehind @@ -570,6 +576,9 @@ treeInit.aspx + + + @@ -595,7 +604,6 @@ - umbraco.aspx @@ -629,6 +637,7 @@ + 404handlers.config @@ -1651,8 +1660,6 @@ - - @@ -1695,7 +1702,7 @@ - + @@ -1856,6 +1863,7 @@ + @@ -1952,7 +1960,9 @@ - + + Designer + Designer @@ -2004,21 +2014,14 @@ - UserControl - - - - UserControl - UserControl - @@ -2227,7 +2230,6 @@ - @@ -2239,15 +2241,12 @@ Form - Form - - @@ -2258,7 +2257,6 @@ - Form @@ -2391,6 +2389,10 @@ + + + + 11.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 @@ -2399,10 +2401,10 @@ - - - xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\amd64\*.* "$(TargetDir)amd64\" /Y /F /E /D -xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" /Y /F /E /D + xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\amd64\*.* "$(TargetDir)amd64\" /Y /F /E /I /C /D +xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" /Y /F /E /I /C /D + + @@ -2412,9 +2414,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 7300 + 7500 / - http://localhost:7300 + http://localhost:7500 False False @@ -2424,7 +2426,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" - + @@ -2451,9 +2453,4 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/Images/editor/renderbody.gif b/src/Umbraco.Web.UI/Umbraco/Images/editor/renderbody.gif new file mode 100644 index 0000000000..1a84493fe9 Binary files /dev/null and b/src/Umbraco.Web.UI/Umbraco/Images/editor/renderbody.gif differ diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml index bd3b13e319..f7c5954c5a 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -43,8 +43,8 @@ @helper Render(dynamic item) {
  • - - @item.Name + + @item.Name
  • } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index 988641d324..787cfbfcc7 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -75,13 +75,16 @@ else @if (registerModel.MemberProperties != null) { + @* + It will only displays properties marked as "Member can edit" on the "Info" tab of the Member Type. + *@ for (var i = 0; i < registerModel.MemberProperties.Count; i++) { @Html.LabelFor(m => registerModel.MemberProperties[i].Value, registerModel.MemberProperties[i].Name) - @* - By default this will render a textbox but if you want to change the editor template for this property you can + @* + By default this will render a textbox but if you want to change the editor template for this property you can easily change it. For example, if you wanted to render a custom editor for this field called "MyEditor" you would - create a file at ~/Views/Shared/EditorTemplates/MyEditor.cshtml", then you will change the next line of code to + create a file at ~/Views/Shared/EditorTemplates/MyEditor.cshtml", then you will change the next line of code to render your specific editor template like: @Html.EditorFor(m => profileModel.MemberProperties[i].Value, "MyEditor") *@ diff --git a/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml index 3d4f6baa7e..eb09f333e3 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml @@ -64,7 +64,8 @@ diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml index d4116e0be9..1ae28daafe 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml @@ -179,6 +179,12 @@ Navštívit Vítejte + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Název Spravovat názvy hostitelů @@ -375,6 +381,7 @@ Zobrazit stránku při odeslání Rozměr Seřadit + Submit Typ Pro hledání pište... Nahoru @@ -391,6 +398,8 @@ Ano Složka Výsledky hledání + Reorder + I am done reordering Background color @@ -740,6 +749,8 @@ Na této záložce nejsou definovány žádné vlastnosti. Pro vytvoření nové vlastnosti klikněte na odkaz "přidat novou vlastnost" nahoře. + Sort order + Creation date Třídění bylo ukončeno. Abyste nastavili, jak mají být položky seřazeny, přetáhněte jednotlivé z nich nahoru či dolů. Anebo klikněte na hlavičku sloupce pro setřídění celé kolekce
    Během třídění nezavírejte toto okno]]>
    @@ -818,6 +829,40 @@ Rychlá příručka k šablonovým značkám umbraca Šablona + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + Columns + Total combined number of columns in the grid layout + Settings + Configure what settings editors can change + Styles + Configure what styling editors can change + Settings will only save if the entered json configuration is valid + Allow all editors + Allow all row configurations + Alternativní pole Alternativní text @@ -899,7 +944,7 @@ Prohlížeč mezipaměti Koš Vytvořené balíčky - Datové typy + Datové typy Slovník Instalované balíčky Instalovat téma @@ -909,10 +954,10 @@ Makra Typy medií Členové - Skupiny členů + Skupiny členů Role - Typy členů - Typy dokumentů + Typy členů + Typy dokumentů Balíčky Balíčky Soubory python diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/no.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml similarity index 62% rename from src/Umbraco.Web.UI/umbraco/config/lang/no.xml rename to src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml index f5d5fe9773..275267e98c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/no.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml @@ -1,819 +1,988 @@ - - - - The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files - - - Angi domene - Revisjoner - Bla gjennom - Kopier - Opprett - Opprett pakke - Slett - Deaktiver - Tøm papirkurv - Eksporter dokumenttype - Importer documenttype - Importer pakke - Rediger i Canvas - Lukk Umbraco - Flytt - Varsling - Offentlig tilgang - Publiser - Oppdater noder - Republiser hele siten - Rettigheter - Reverser - Send til publisering - Send til oversetting - Sorter - Send til publisering - Oversett - Oppdater - Avpubliser - - - Legg til domene - Domene - Domene '%0%' er nå opprettet og tilknyttet siden - Domenet '%0%' er nå slettet - Domenet '%0%' er allerede tilknyttet - Gyldige domenenavn er: "eksempel.no", "www.eksempel.no", "eksempel.no:8080" eller "https://www.eksempel.no/".<br/><br/>Stier med ett nivå støttes, f.eks. "eksempel.com/no". Imidlertid bør det unngås. Bruk heller språkinnstillingen over. - Domenet '%0%' er nå oppdatert - eller rediger eksisterende domener - Ingen tilgang. - Fjern - Ugyldig node. - Ugyldig domeneformat. - Domene er allerede tilknyttet. - Språk - Arv - Språk - Sett språk for underordnede noder eller arv språk fra overordnet.<br/>Vil også gjelde denne noden, med mindre et underordnet domene også gjelder. - Domener - - - Viser for - - - Fet - Reduser innrykk - Sett inn skjemafelt - Sett inn grafisk overskrift - Rediger HTML - Øk innrykk - Kursiv - Midtstill - Juster tekst venstre - Juster tekst høyre - Sett inn lenke - Sett inn lokal lenke (anker) - Punktmerking - Nummerering - Sett inn makro - Sett inn bilde - Rediger relasjoner - Lagre - Lagre og publiser - Lagre og send til publisering - Forhåndsvis - Velg formattering - Vis stiler - Sett inn tabell - Forhåndsvisning er deaktivert siden det ikke er angitt noen mal - - - Om siden - Alternativ lenke - (hvordan du ville beskrevet bildet over telefon) - Alternative lenker - Klikk for å redigere denne noden - Opprettet av - Opprettet den - Dokumenttype - Redigerer - Utløpsdato - Denne noden er endret siden siste publisering - Denne noden er enda ikke publisert - Sist publisert - Mediatype - Medlemsgruppe - Rolle - Medlemstype - Ingen dato valgt - Sidetittel - Egenskaper - Dette dokumentet er publisert, men ikke synlig ettersom den overliggende siden '%0%' ikke er publisert - Publisert - Publiseringsstatus - Publiseringsdato - Fjern dato - Sorteringsrekkefølgen er oppdatert - Trekk og slipp nodene eller klikk på kolonneoverskriftene for å sortere. Du kan velge flere noder ved å holde shift eller control tastene mens du velger. - Statistikk - Tittel (valgfri) - Type - Avpubliser - Sist endret - Fjern fil - Lenke til dokument - Link til media - Intern feil: dokumentet er publisert men finnes ikke i hurtigbuffer - - - Hvor ønsker du å oprette den nye %0% - Opprett under - Velg en type og skriv en tittel - - - Til ditt nettsted - - Skjul - Hvis Umbraco ikke starter, kan det skyldes at pop-up vinduer ikke er tillatt - er åpnet i nytt vindu - Omstart - Besøk - Velkommen - - - Navn på lokal link - Rediger domener - Lukk dette vinduet - Er du sikker på at du vil slette - Er du sikker på at du vil deaktivere - Vennligst kryss av i denne boksen for å bekrefte sletting av %0% element(er) - Er du sikker på at du vil forlate Umbraco? - Er du sikker? - Klipp ut - Rediger ordboksnøkkel - Rediger språk - Sett inn lokal link - Sett inn spesialtegn - Sett inn grafisk overskrift - Sett inn bilde - Sett inn lenke - Sett inn makro - Sett inn tabell - Sist redigert - Lenke - Intern link: - Ved lokal link, sett inn "#" foran link - Åpne i nytt vindu? - Makroinnstillinger - Denne makroen har ingen egenskaper du kan endre - Lim inn - Endre rettigheter for - Innholdet i papirkurven blir nå slettet. Vennligst ikke lukk dette vinduet mens denne operasjonen foregår - Papirkurven er nå tom - Når elementer blir slettet fra papirkurven vil de være slettet for alltid - <a target='_blank' href='http://regexlib.com'>regexlib.com</a> tjenesten opplever for tiden problemer som vi ikke har kontroll over. Vi beklager denne ubeleiligheten. - Søk etter et regulært uttrykk for å legge inn validering til et felt. Eksempel: 'email, 'zip-code' 'url' - Fjern makro - Obligatorisk - Nettstedet er indeksert - Hurtigbufferen er blitt oppdatert. Alt publisert innhold er nå à jour. Alt upublisert innhold er fortsatt ikke publisert. - Hurtigbufferen for siden vil bli oppdatert. Alt publisert innhold vil bli oppdatert, mens upublisert innhold vil forbli upublisert. - Antall kolonner - Antall rader - <strong>Sett en plassholder-ID</strong><br/>Ved å sette en ID på plassholderen kan du legge inn innhold i denne malen fra underliggende maler, ved å referere denne ID'en ved hjelp av et <code>&lt;asp:content /&gt;</code> element. - <strong>Velg en plassholder ID</strong> fra listen under. Du kan bare velge ID'er fra den gjeldende malens overordnede mal. - Klikk på bildet for å se det i full størrelse - Velg punkt - Se buffret node - - - Rediger de forskjellige språkversjonene for ordbokelementet '<em>%0%</em>' under.<br/>Du kan legge til flere språk under 'språk' i menyen til venstre. - Språk - - - Tillatte underordnede noder - Opprett - Slett arkfane - Beskrivelse - Ny arkfane - Arkfane - Miniatyrbilde - - - Legg til forhåndsverdi - Database datatype - Kontrollelement GUID - Kontrollelement - Knapper - Aktiver avanserte instillinger for - Aktiver kontektsmeny - Maksimum standard størrelse på innsatte bilder - Beslektede stilark - Vis etikett - Bredde og høyde - - - Dine data har blitt lagret, men før du kan publisere denne siden må du rette noen feil: - Den gjeldende Membership Provider støtter ikke endring av passord. (EnablePasswordRetrieval må være satt til sann) - %0% finnes allerede - Det var feil i dokumentet: - Det var feil i skjemaet: - Passordet bør være minst %0% tegn og inneholde minst %1% numeriske tegn - %0% må være et heltall - %0% under %1% er obligatorisk - %0% er obligatorisk - %0% under %1% er ikke i et korrekt format - %0% er ikke i et korrekt format - - - NB! Selv om CodeMirror er aktivert i konfigurasjon er det deaktivert i Internet Explorer pga. ustabilitet. - Fyll ut både alias og navn på den nye egenskapstypen! - Det er et problem med lese/skrive rettighetene til en fil eller mappe - Tittel mangler - Type mangler - Du er i ferd med å gjøre bildet større enn originalen. Det vil forringe kvaliteten på bildet, ønsker du å fortsette? - Feil i python-skriptet - Python-skriptet ble ikke lagret fordi det inneholder en eller flere feil - Startnode er slettet. Kontakt din administrator - Du må markere innhold før du kan endre stil - Det er ingen aktive stiler eller formateringer på denne siden - Sett markøren til venstre i de 2 cellene du ønsker å slå sammen - Du kan ikke dele en celle som allerede er delt. - Feil i XSLT kode - XSLT ble ikke lagret på grunn av feil i koden - Filtypen er deaktivert av administrator - - - Om - Handling - Legg til - Alias - Er du sikker? - Ramme - eller - Avbryt - Cellemargin - Velg - Lukk - Lukk vindu - Kommentar - Bekreft - Behold proposjoner - Fortsett - Kopier - Opprett - Database - Dato - Standard - Slett - Slettet - Sletter... - Design - Dimensjoner - Ned - Last ned - Rediger - Endret - Elementer - E-post - Feil - Finn - Høyde - Hjelp - Ikon - Importer - Indre margin - Sett inn - Installer - Justering - Språk - Layout - Laster - Låst - Logg inn - Logg ut - Logg ut - Makro - Flytt - Navn - Ny - Neste - Nei - av - OK - Åpne - eller - Passord - Sti - Plassholder ID - Ett øyeblikk... - Forrige - Egenskaper - E-post som innholdet i skjemaet skal sendes til - Papirkurv - Gjenværende - Gi nytt navn - Forny - Prøv igjen - Rettigheter - Søk - Server - Vis - Hvilken side skal vises etter at skjemaet er sendt - Størrelse - Sorter - Type - Søk... - Opp - Oppdater - Oppgrader - Last opp - Url - Bruker - Brukernavn - Verdi - Visning - Velkommen... - Bredde - Ja - Mappe - - - Bakgrunnsfarge - Fet - Tekstfarge - Skrifttype - Tekst - - - Side - - - Installasjonsprogrammet kan ikke koble til databasen - Kunne ikke lagre Web.Config-filen. Vennligst endre databasens tilkoblingsstreng manuelt. - Din database er funnet og identifisert som - Databasekonfigurasjon - Klikk <strong>installer</strong>-knappen for å installere Umbraco %0% databasen - Umbraco %0% har nå blitt kopiert til din database. Trykk <strong>Neste</strong> for å fortsette. - <p>Databasen ble ikke funnet! Vennligst sjekk at informasjonen i "connection string" i "web.config"-filen er korrekt.</p><p>For å fortsette, vennligst rediger "web.config"-filen (bruk Visual Studio eller din favoritteditor), rull ned til bunnen, og legg til tilkoblingsstrengen for din database i nøkkelen "umbracoDbDSN" og lagre filen.</p><p>Klikk <strong>prøv på nytt</strong> når du er ferdig.<br /> <a href="http://our.umbraco.org/documentation/Using-Umbraco/Config-files/webconfig7" target="_blank">Mer informasjon om redigering av web.config her.</a></p> - For å fullføre dette steget, må du vite en del informasjon om din database server ("tilkoblingsstreng").<br/> Vennligst kontakt din ISP om nødvendig. Hvis du installerer på en lokal maskin eller server, må du kanskje skaffe informasjonen fra din systemadministrator. - <p> Trykk på knappen <strong>oppgrader</strong> for å oppgradere databasen din til Umbraco %0%</p> <p> Ikke vær urolig - intet innhold vil bli slettet og alt vil fortsette å virke etterpå! </p> - Databasen din har blitt oppgradert til den siste utgaven, %0%.<br/>Trykk <strong>Neste</strong> for å fortsette. - Databasen din er av nyeste versjon! Klikk <strong>neste</strong> for å fortsette konfigurasjonsveiviseren - <strong>Passordet til standardbrukeren må endres! - <strong>Standardbrukeren har blitt deaktivert eller har ingen tilgang til Umbraco!</strong></p><p>Ingen videre handling er nødvendig. Klikk <b>neste</b> for å fortsette. - <strong>Passordet til standardbrukeren har blitt forandret etter installasjonen!</strong></p><p>Ingen videre handling er nødvendig. Klikk <strong>Neste</strong> for å fortsette. - Passordet er blitt endret! - <p> Umbraco skaper en standard bruker med login <strong> ( "admin") </ strong> og passord <strong> ( "default") </ strong>. Det er <strong> viktig </ strong> at passordet er endret til noe unikt. </ p> <p> Dette trinnet vil sjekke standard brukerens passord og foreslår hvis det må skiftes </ p> - Få en god start med våre introduksjonsvideoer - Ved å klikke på Neste-knappen (eller endre UmbracoConfigurationStatus i Web.config), godtar du lisensen for denne programvaren som angitt i boksen nedenfor. Legg merke til at denne Umbraco distribusjon består av to ulike lisenser, åpen kilde MIT lisens for rammen og Umbraco frivareverktøy lisens som dekker brukergrensesnittet. - Ikke installert. - Berørte filer og mapper - Mer informasjon om å sette opp rettigheter for Umbraco her - Du må gi ASP.NET brukeren rettigheter til å endre de følgende filer og mapper - <strong>Rettighetene er nesten perfekt satt opp!</strong><br/><br/> Du kan kjøre Umbraco uten problemer, men du vil ikke være i stand til å installere de anbefalte pakkene for å utnytte Umbraco fullt ut. - Hvordan løse problemet - Klikk her for å lese tekstversjonen - Se vår <strong>innføringsvideo</strong> om å sette opp rettigheter for Umbraco eller les tekstversjonen. - <strong>Rettighetsinnstillingene kan være et problem!</strong><br/><br/> Du kan kjøre Umbraco uten problemer, men du vil ikke være i stand til å installere de anbefalte pakkene for å utnytte Umbraco fullt ut. - <strong>Rettighetsinstillingene er ikke klargjort for Umbraco!</strong><br/><br/> For å kunne kjøre Umbraco, må du oppdatere rettighetsinnstillingene dine. - <strong>Rettighetsinnstillingene er perfekt!</strong><br/><br/>Du er klar for å kjøre Umbraco og installere pakker! - Løser mappeproblem - Følg denne linken for mer informasjon om problemer med ASP.NET og oppretting av mapper - Konfigurerer mappetillatelser - Umbraco trenger skrive/endre tilgang til enkelte mapper for å kunne lagre filer som bilder og PDF-dokumenter. Den lagrer også midlertidig data (aka: hurtiglager) for å øke ytelsen på websiden din. - Jeg ønsker å starte fra bunnen. - Din website er helt tom for øyeblikket. Dette er perfekt hvis du vil begynne helt forfra og lage dine egne dokumenttyper og maler. (<a href="http://Umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">lær hvordan</a>) Du kan fortsatt velge å installere Runway senere. Vennligst gå til Utvikler-seksjonen og velg Pakker. - Du har akkurat satt opp en ren Umbraco plattform. Hva vil du gjøre nå? - Runway er installert - Du har nå fundamentet på plass. Velg hvilke moduler du ønsker å installer på toppen av det.<br/> Dette er vår liste av anbefalte moduler- Kryss av de du ønsker å installere, eller se den<a href="#" onclick="toggleModules(); return false;" id="toggleModuleList">fulle listen av moduler</a> - Bare anbefalt for erfarne brukere - Jeg vil starte med en enkel webside - <p> "Runway" er en enkel webside som utstyrer deg med noen grunnleggende dokumenttyper og maler. Veiviseren kan sette opp Runway for deg automatisk, men du kan enkelt endre, utvide eller slette den. Runway er ikke nødvendig, og du kan enkelt bruke Umbraco uten den. Imidlertidig tilbyr Runway et enkelt fundament basert på de beste metodene for å hjelpe deg i gang fortere enn noensinne. Hvis du velger å installere Runway, kan du også velge blant grunnleggende byggeklosser kalt Runway Moduler for å forøke dine Runway-sider. </p> <small> <em>Sider inkludert i Runway:</em> Hjemmeside, Komme-i-gang, Installere moduler.<br /> <em>Valgfrie Moduler:</em> Toppnavigasjon, Sidekart, Kontakt, Galleri. </small> - Hva er Runway - Steg 1/5 Godta lisens - Steg 2/5 Database konfigurasjon - Steg 3/5: Valider filrettigheter - Steg 4/5: Skjekk Umbraco sikkerheten - Steg 5/5: Umbraco er klar for deg til å starte! - Tusen takk for at du valgte Umbraco! - <h3>Se ditt nye nettsted</h3> Du har installert Runway, hvorfor ikke se hvordan ditt nettsted ser ut. - <h3>Mer hjelp og info</h3> Få hjelp fra vårt prisbelønte samfunn, bla gjennom dokumentasjonen eller se noen gratis videoer på hvordan man bygger et enkelt nettsted, hvordan bruke pakker og en rask guide til Umbraco terminologi - Umbraco %0% er installert og klar til bruk - For å fullføre installasjonen, må du manuelt endre <strong>web.config</strong> filen, og oppdatere AppSetting-nøkkelen <strong>UmbracoConfigurationStatus</strong> til verdien <strong>'%0%'</strong> - Du kan <strong>starte øyeblikkelig</strong> ved å klikke på "Start Umbraco" knappen nedenfor. <br/>Hvis du er <strong>ny på Umbraco</strong>, kan du finne mange ressurser på våre komme-i-gang sider. - <h3>Start Umbraco</h3> For å administrere din webside, åpne Umbraco og begynn å legge til innhold, oppdatere maler og stilark eller utvide funksjonaliteten - Tilkobling til databasen mislyktes. - Umbraco Versjon 3 - Umbraco Versjon 4 - Pass på - Denne veiviseren vil hjelpe deg gjennom prosessen med å konfigurere <strong>Umbraco %0%</strong> for en ny installasjon eller oppgradering fra versjon 3.0. <br/><br/> Trykk <strong>"neste"</strong> for å starte veiviseren. - - - Språkkode - Språk - - - Du har vært inaktiv og vil logges ut automatisk om - Forny innlogging for å lagre - - - <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> - Velkommen til Umbraco, skriv inn ditt brukernavn og passord i feltene under: - - - Skrivebord - Seksjoner - Innhold - - - Velg side over... - %0% er nå kopiert til %1% - Kopier til - %0% er nå flyttet til %1% - Flytt til - har blitt valgt som rot til ditt nye innhold, klikk 'ok' nedenfor. - Ingen node er valgt, vennligst velg en node i listen over før du klikker 'fortsett' - Gjeldende nodes type tillates ikke under valgt node - Gjeldende node kan ikke legges under en underordnet node - Handlingen tillates ikke. Du mangler tilgang til en eller flere underordnede noder. - Relater kopierte elementer til original(e) - - - Rediger dine varsler for %0% - -Hei %0% - -Dette er en automatisk mail for å informere om at handlingen '%1%' -er utført på siden '%2%' -av brukeren '%3%' - -Gå til http://%4%/Umbraco/default.aspx?section=content&id=%5% for å redigere. - -Ha en fin dag! - -Vennlig hilsen Umbraco roboten - - <p>Hei %0%</p> - - <p>Dette er en automatisk mail for å informere om at handlingen '%1%' - er blitt utført på siden <a href="http://%4%/actions/preview.aspx?id=%5%"><strong>'%2%'</strong></a> - av brukeren <strong>'%3%'</strong> - </p> - <div style="margin: 8px 0; padding: 8px; display: block;"> - <br /> - <a style="color: white; font-weight: bold; background-color: #5372c3; text-decoration : none; margin-right: 20px; border: 8px solid #5372c3; width: 150px;" href="http://%4%/Umbraco/actions/editContent.aspx?id=%5%">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REDIGER&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</a> &nbsp; - <br /> - </div> - <p> - <h3>Rettelser:</h3> - <table style="width: 100%;"> - %6% - </table> - </p> - - <div style="margin: 8px 0; padding: 8px; display: block;"> - <br /> - <a style="color: white; font-weight: bold; background-color: #5372c3; text-decoration : none; margin-right: 20px; border: 8px solid #5372c3; width: 150px;" href="http://%4%/Umbraco/actions/editContent.aspx?id=%5%">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REDIGER&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</a> &nbsp; - <br /> - </div> - - <p>Ha en fin dag!<br /><br /> - Vennlig hilsen Umbraco roboten - </p> - [%0%] Varsling om %1% utført på %2% - Varsling - - - Klikke browse og velg pakke fra lokal disk. Umbraco-pakker har vanligvis endelsen ".umb" eller ".zip". - Utvikler - Demonstrasjon - Dokumentasjon - Metadata - Pakkenavn - Pakken inneholder ingen elementer - Denne pakkefilen inneholder ingen elementer å avinstallere.<br/><br/>Du kan trygt fjerne pakken fra systemet ved å klikke "avinstaller pakke" nedenfor. - Ingen oppdateringer tilgjengelig - Alternativer for pakke - Lesmeg for pakke - Pakkebrønn - Bekreft avinstallering - Pakken ble avinstallert - Pakken ble vellykket avinstallert - Avinstaller pakke - Du kan velge bort elementer du ikke vil slette på dette tidspunkt, nedenfor. Når du klikker "bekreft avinstallering" vil alle elementer som er krysset av bli slettet.<br/> <span style="color:red;font-weight:bold;">Advarsel:</span> alle dokumenter, media, etc. som som er avhengig av elementene du sletter, vil slutte å virke, noe som kan føre til ustabilitet, så avinstaller med forsiktighet. Hvis du er i tvil, kontakt pakkeutvikleren. - Last ned oppdatering fra pakkeregisteret - Oppgrader pakke - Oppgraderingsinstrukser - Det er en oppdatering tilgjengelig for denne pakken. Du kan laste den ned direkte fra pakkebrønnen. - Pakkeversjon - Se pakkens nettsted - - - Lim inn med full formattering (Anbefales ikke) - Teksten du er i ferd med å lime inn, inneholder spesialtegn eller formattering. Dette kan skyldes at du kopierer fra f.eks. Microsoft Word. Umbraco kan fjerne denne spesialformatteringen automatisk slik at innholdet er mer velegnet for visning på en webside. - Lim inn som ren tekst, dvs. fjern al formattering - Lim inn og fjern uegnet formatering (anbefalt) - - - Avansert: Beskytt ved å velge hvilke brukergrupper som har tilgang til siden - Om du ønsker å kontrollere tilgang til siden ved å bruke rolle-basert autentisering,<br /> ved å bruke Umbraco's medlems-grupper - Du må opprette en medlemsgruppe før du kan bruke <br /> rollebasert autentikasjon. - Feilside - Brukt når personer logger på, men ikke har tilgang - Hvordan vil du beskytte siden din? - %0% er nå beskyttet - Beskyttelse fjernet fra %0% - Innloggingsside - Velg siden som har loginformularet - Fjern beskyttelse - Velg sidene som inneholder login-skjema og feilmelding ved feil innolgging. - Velg rollene som har tilgang til denne siden - Sett brukernavn og passord for denne siden - Enkelt: Beskytt ved hjelp av brukernavn og passord - Om du ønsker å bruke enkel autentisering via ett enkelt brukernavn og passord - - - %0% kunne ikke publiseres fordi et tredjepartstillegg avbrøt handlingen. - Inkluder upubliserte undersider - Publiserer - vennligst vent... - %0% av %1% sider har blitt publisert... - %0% er nå publisert - %0% og alle undersider er nå publisert - Publiser alle undersider - Klikk <em>ok</em> for å publisere <strong>%0%</strong> og dermed gjøre innholdet synlig for alle.<br/><br />Du kan publisere denne siden og alle dens undersider ved å krysse av <em>Publiser alle undersider</em> nedenfor. - %0% ble ikke publisert. Ett eller flere felter ble ikke godkjent av validering. - %0% kan ikke publiseres fordi en overordnet side ikke er publisert. - - - Legg til ekstern lenke - Legg til intern lenke - Legg til - Tittel - Intern side - Url - Flytt ned - Flytt opp - Åpne i nytt vindu - Fjern lenke - - - Gjeldende versjon - Dette viser forskjellene mellom den gjeldende og den valgte versjonen<br /><del>Rød</del> tekst vil ikke bli vist i den valgte versjonen. , <ins>grønn betyr lagt til</ins> - Dokumentet er tilbakeført til en tidligere versjon - Dette viser den valgte versjonen som HTML, bruk avviksvisningen hvis du ønsker å se forksjellene mellom to versjoner samtidig. - Tilbakefør til - Velg versjon - Vis - - - Rediger scriptfilen - - - Concierge - Innhold - Courier - Utvikler - Umbraco konfigurasjonsveiviser - Mediaarkiv - Medlemmer - Nyhetsbrev - Innstillinger - Statistikk - Oversettelse - Brukere - - - Standardmal - Ordboksnøkkel - For å importere en dokumenttype, finn ".udt" filen på datamaskinen din ved å klikke "Utforsk" knappen og klikk "Importer" (du vil bli spurt om bekreftelse i det neste skjermbildet) - Ny tittel på arkfane - Nodetype - Type - Stilark - Stilark-egenskap - Arkfane - Tittel på arkfane - Arkfaner - Hovedinnholdstype aktivert - Denne dokumenttypen bruker - som hoveddokumenttype. Arkfaner fra hoveddokumenttyper vises ikke og kan kun endres på hoveddokumenttypen selv. - - - Sortering ferdig. - Dra elementene opp eller ned for å arrangere dem. Du kan også klikke kolonneoverskriftene for å sortere alt på en gang. - Vennligst vent. Elementene blir sortert, dette kan ta litt tid.<br/> <br/> Ikke lukk dette vinduet under sortering - - - Publisering ble avbrutt av et tredjepartstillegg - Egenskaptypen finnes allerede - Egenskapstype opprettet - Navn: %0% <br /> DataType: %1% - Egenskapstype slettet - Innholdstype lagret - Du har opprettet en arkfane - Arkfane slettet - Arkfane med id: %0% slettet - Stilarket ble ikke lagret - Stilarket ble lagret - Stilark lagret uten feil - Datatype lagret - Ordbokelement lagret - Publiseringen feilet fordi den overliggende siden ikke er publisert - Innhold publisert - og er nå synlig for besøkende - Innhold lagret - Husk å publisere for å gjøre endringene synlig for besøkende - Sendt for godkjenning - Endringer har blitt sendt til godkjenning - Medlem lagret - Stilarksegenskap lagret - Stilark lagret - Mal lagret - Feil ved lagring av bruker (sjekk loggen) - Bruker lagret - Filen ble ikke lagret - Filen kunne ikke lagres. Vennligst sjekk filrettigheter - Filen ble lagret - Filen ble lagret uten feil - Språk lagret - Python-skriptet ble ikke lagret - Python-skriptet kunne ikke lagres fordi det inneholder en eller flere feil - Python-skriptet er lagret! - Ingen feil i python-skriptet! - Malen ble ikke lagret - Vennligst forviss deg om at du ikke har to maler med samme alias - Malen ble lagret - Malen ble lagret uten feil! - XSLT-koden ble ikke lagret - XSLT-koden inneholdt en feil - XSLT-koden ble ikke lagret, sjekk filrettigheter - XSLT lagret - Ingen feil i XSLT! - Media lagret - Brukertypen lagret - Innhold avpublisert - Delmal lagret - Delmal lagret uten feil - Delmal ble ikke lagret! - En feil oppsto ved lagring av delmal - - - Bruk CSS syntaks f.eks: h1, .redHeader, .blueText - Rediger stilark - Rediger egenskap for stilark - Navn for å identifisere stilarksegenskapen i rik-tekst editoren - Forhåndsvis - Stiler - - - Rediger mal - Sett inn innholdsområde - Sett inn plassholder for innholdsområde - Sett inn ordbokselement - Sett inn makro - Sett inn Umbraco sidefelt - Hovedmal - Hurtigguide til Umbraco sine maltagger - Mal - - - Alternativt felt - Alternativ tekst - Store/små bokstaver - Felt som skal settes inn - Konverter linjeskift - Erstatter et linjeskift med htmltaggen &lt;br&gt; - Ja, kun dato - Formatter som dato - HTML koding - Formater spesialtegn med tilsvarende HTML-tegn. - Denne teksten vil settes inn etter verdien av feltet - Denne teksten vil settes inn før verdien av feltet - Små bokstaver - Ingen - Sett inn etter felt - Sett inn før felt - Rekursivt - Fjern paragraftagger - Fjerner eventuelle &lt;P&gt; rundt teksten - Store bokstaver - URL koding - Dersom innholdet av feltene skal sendes til en URL skal spesialtegn formatteres - Denne teksten vil benyttes dersom feltene over er tomme - Dette feltet vil benyttes dersom feltet over er tomt - Ja, med klokkeslett. Dato/tid separator: - Egendefinerte felt - Standardfelter - - - Oppgaver satt til deg - Listen nedenfor viser oversettelsesoppgaver <strong>som du er tildelt</strong>. For å se en detaljert visning inkludert kommentarer, klikk på "Detaljer" eller navnet på siden. Du kan også laste ned siden som XML direkte ved å klikke på linken "Last ned XML". <br/> For å lukke en oversettelsesoppgave, vennligst gå til detaljvisningen og klikk på "Lukk" knappen. - Lukk oppgave - Oversettelses detaljer - Last ned all oversettelsesoppgaver som XML - Last ned XML - Last ned XML DTD - Felt - Inkluder undersider - - Hei %0% - - Dette er en automatisk mail for å informere deg om at dokumentet '%1%' - har blitt anmodet oversatt til '%5%' av %2%. - - Gå til http://%3%/Umbraco/translation/default.aspx?id=%4% for å redigere. - - Ha en fin dag! - - Vennlig hilsen Umbraco Robot. - - [%0%] Oversettingsoppgave for %1% - Ingen oversettelses-bruker funnet. Vennligst opprett en oversettelses-bruker før du begynner å sende innhold til oversetting - Oppgaver opprettet av deg - Listen under viser sider <strong>opprettet av deg</strong>. For å se en detaljert visning inkludert kommentarer, klikk på "Detaljer" eller navnet på siden. Du kan også laste ned siden som XML direkte ved å klikke på linken "Last ned XML". For å lukke en oversettelsesoppgave, vennligst gå til detaljvisningen og klikk på "Lukk" knappen. - Siden '%0%' har blitt sendt til oversetting - Send til oversetting - Tildelt av - Oppgave åpnet - Antall ord - Oversett til - Oversetting fullført. - Du kan forhåndsvise sidene du nettopp har oversatt ved å klikke nedenfor. Hvis den originale siden finnes, vil du få en sammenligning av sidene. - Oversetting mislykkes, XML filen kan være korrupt - Alternativer for oversetting - Oversetter - Last opp XML med oversettelse - - - Hurtigbufferleser - Papirkurv - Opprettede pakker - Datatyper - Ordbok - Installerte pakker - Installer utseende - Installer startpakke - Språk - Installer lokal pakke - Makroer - Mediatyper - Medlemmer - Medlemsgrupper - Roller - Medlemstyper - Dokumenttyper - Pakker - Pakker - Python Filer - Installer fra pakkeregister - Installer Runway - Runway moduler - Skriptfiler - Skript - Stiler - Maler - XSLT Filer - - - Ny oppdatering er klar - %0% er klar, klikk her for å laste ned - Ingen forbindelse til server - Kunne ikke sjekke etter ny oppdatering. Se trace for mere info. - - - Administrator - Kategorifelt - Bytt passord - Du kan endre passordet til Umbraco ved å fylle ut skjemaet under og klikke "Bytt passord" knappen. - Innholdskanal - Beskrivelsesfelt - Deaktiver bruker - Dokumenttype - Redaktør - Utdragsfelt - Språk - Login - Øverste nivå i Media - Moduler - Deaktiver tilgang til Umbraco - Passord - Passordet er endret - Bekreft nytt passord - Nytt passord - Nytt passord kan ikke være blankt - Nytt og bekreftet passord må være like - Nytt og bekreftet passord må være like - Overskriv tillatelser på undernoder - Du redigerer for øyeblikket tillatelser for sidene: - Velg sider for å redigere deres tillatelser - Søk i alle undersider - Startnode - Brukernavn - Brukertillatelser - Brukertype - Brukertyper - Forfatter - Nytt passord - Bekreft nytt passord - Gjeldende passord - Feil passord - + + + + The Umbraco community + http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + + + Angi domene + Revisjoner + Bla gjennom + Skift dokumenttype + Kopier + Opprett + Opprett pakke + Slett + Deaktiver + Tøm papirkurv + Eksporter dokumenttype + Importer dokumenttype + Importer pakke + Rediger i Canvas + Logg av + Flytt + Varslinger + Offentlig tilgang + Publiser + Avpubliser + Oppdater noder + Republiser hele siten + Gjenopprett + Rettigheter + Reverser + Send til publisering + Send til oversetting + Sorter + Send til publisering + Oversett + Oppdater + Standard verdi + + + Ingen tilgang. + Legg til domene + Fjern + Ugyldig node. + Ugyldig domeneformat. + Domene er allerede tilknyttet. + Språk + Domene + Domene '%0%' er nå opprettet og tilknyttet siden + Domenet '%0%' er nå slettet + Domenet '%0%' er allerede tilknyttet + Domenet '%0%' er nå oppdatert + eller rediger eksisterende domener +
    Stier med ett nivå støttes, f.eks. "eksempel.com/no". Imidlertid bør det unngås. Bruk heller språkinnstillingen over.]]>
    + Arv + Språk + Vil også gjelde denne noden, med mindre et underordnet domene også gjelder.]]> + Domener + + + Viser for + + + Velg + Velg gjeldende mappe + Gjør noe annet + Fet + Reduser innrykk + Sett inn skjemafelt + Sett inn grafisk overskrift + Rediger HTML + Øk innrykk + Kursiv + Midtstill + Juster tekst venstre + Juster tekst høyre + Sett inn lenke + Sett inn lokal lenke (anker) + Punktmerking + Nummerering + Sett inn makro + Sett inn bilde + Rediger relasjoner + Tilbake til listen + Lagre + Lagre og publiser + Lagre og send til publisering + Forhåndsvis + Forhåndsvisning er deaktivert siden det ikke er angitt noen mal + Velg formattering + Vis stiler + Sett inn tabell + + + For å endre det valge innholdets dokumenttype, velger du først en ny dokumenttype som er gyldig på gjeldende plassering. + Kontroller deretter at alle egenskaper blir overført riktig til den nye dokumenttypen og klikk på Lagre. + Innholdet har blitt republisert. + Nåværende egenskap + Nåværende type + Du kan ikke endre dokumenttype, ettersom det ikke er andre gyldige dokumenttyper på denne plasseringen. + Dokumenttype endret + Overfør egenskaper + Overfør til egenskap + Ny mal + Ny type + ingen + Innhold + Velg ny dokumenttype + Dokumenttypen på det valgte innhold ble endret til [new type], og følgende egenskaper ble overført: + til + Overføringen av egenskaper kunne ikke fullføres da en eller flere egenskaper er satt til å bli overført mer enn en gang. + Kun andre dokumenttyper som er gyldige for denne plasseringen vises. + + + Publisert + Om siden + Alias + (hvordan du ville beskrevet bildet over telefon) + Alternative lenker + Klikk for å redigere denne noden + Opprettet av + Opprinnelig forfatter + Oppdatert av + Opprettet den + Tidspunkt for opprettelse + Dokumenttype + Redigerer + Utløpsdato + Denne noden er endret siden siste publisering + Denne noden er enda ikke publisert + Sist publisert + Det er ingen elementer å vise i listen. + Mediatype + Link til media + Medlemsgruppe + Rolle + Medlemstype + Ingen dato valgt + Sidetittel + Egenskaper + Dette dokumentet er publisert, men ikke synlig ettersom den overliggende siden '%0%' ikke er publisert + Intern feil: dokumentet er publisert men finnes ikke i hurtigbuffer + Publisert + Publiseringsstatus + Publiseringsdato + Dato for avpublisering + Fjern dato + Sorteringsrekkefølgen er oppdatert + Trekk og slipp nodene eller klikk på kolonneoverskriftene for å sortere. Du kan velge flere noder ved å holde shift eller control tastene mens du velger. + Statistikk + Tittel (valgfri) + Alternativ tekst (valgfri) + Type + Avpubliser + Sist endret + Tidspunkt for siste endring + Fjern fil + Lenke til dokument + Medlem av gruppe(ne) + Ikke medlem av gruppe(ne) + Undersider + Åpne i vindu + + + Klikk for å laste opp + Slipp filene her... + + + Opprett et nytt medlem + Alle medlemmer + + + Hvor ønsker du å oprette den nye %0% + Opprett under + Velg en type og skriv en tittel + "dokumenttyper".]]> + "mediatyper".]]> + + + Til ditt nettsted + - Skjul + Hvis Umbraco ikke starter, kan det skyldes at pop-up vinduer ikke er tillatt + er åpnet i nytt vindu + Omstart + Besøk + Velkommen + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + + + Navn på lokal link + Rediger domener + Lukk dette vinduet + Er du sikker på at du vil slette + Er du sikker på at du vil deaktivere + Vennligst kryss av i denne boksen for å bekrefte sletting av %0% element(er) + Er du sikker på at du vil forlate Umbraco? + Er du sikker? + Klipp ut + Rediger ordboksnøkkel + Rediger språk + Sett inn lokal link + Sett inn spesialtegn + Sett inn grafisk overskrift + Sett inn bilde + Sett inn lenke + Sett inn makro + Sett inn tabell + Sist redigert + Lenke + Intern link: + Ved lokal link, sett inn "#" foran link + Åpne i nytt vindu? + Makroinnstillinger + Denne makroen har ingen egenskaper du kan endre + Lim inn + Endre rettigheter for + Innholdet i papirkurven blir nå slettet. Vennligst ikke lukk dette vinduet mens denne operasjonen foregår + Papirkurven er nå tom + Når elementer blir slettet fra papirkurven vil de være slettet for alltid + regexlib.com tjenesten opplever for tiden problemer som vi ikke har kontroll over. Vi beklager denne ubeleiligheten.]]> + Søk etter et regulært uttrykk for å legge inn validering til et felt. Eksempel: 'email, 'zip-code' 'url' + Fjern makro + Obligatorisk + Nettstedet er indeksert + Hurtigbufferen er blitt oppdatert. Alt publisert innhold er nå à jour. Alt upublisert innhold er fortsatt ikke publisert. + Hurtigbufferen for siden vil bli oppdatert. Alt publisert innhold vil bli oppdatert, mens upublisert innhold vil forbli upublisert. + Antall kolonner + Antall rader + Sett en plassholder-ID
    Ved å sette en ID på plassholderen kan du legge inn innhold i denne malen fra underliggende maler, ved å referere denne ID'en ved hjelp av et <asp:content /> element.]]>
    + Velg en plassholder ID fra listen under. Du kan bare velge ID'er fra den gjeldende malens overordnede mal.]]> + Klikk på bildet for å se det i full størrelse + Velg punkt + Se buffret node + + + %0%' under.
    Du kan legge til flere språk under 'språk' i menyen til venstre.]]>
    + Språk + + + Skriv inn ditt brukernavn + Skriv inn ditt passord + Navngi %0%... + Skriv inn navn... + Søk... + Filtrer... + Skriv inn nøkkelord (trykk på Enter etter hvert nøkkelord)... + + + Tillat på rotnivå + Kun dokumenttyper med denne innstillingen aktivert kan opprettes på rotnivå under Innhold og Mediearkiv + Tillatte underordnede noder + Sammensetting av dokumenttyper + Opprett + Slett arkfane + Beskrivelse + Ny arkfane + Arkfane + Miniatyrbilde + Aktiver listevisning + Viser undersider i en søkbar liste, undersider vises ikke i innholdstreet + Gjeldende listevisning + Den aktive listevisningsdatatypen + Opprett brukerdefinert listevisning + Fjern brukerdefinert listevisning + + + Legg til forhåndsverdi + Database datatype + Kontrollelement GUID + Kontrollelement + Knapper + Aktiver avanserte instillinger for + Aktiver kontektsmeny + Maksimum standard størrelse på innsatte bilder + Beslektede stilark + Vis etikett + Bredde og høyde + + + Dine data har blitt lagret, men før du kan publisere denne siden må du rette noen feil: + Den gjeldende Membership Provider støtter ikke endring av passord. (EnablePasswordRetrieval må være satt til sann) + %0% finnes allerede + Det var feil i dokumentet: + Det var feil i skjemaet: + Passordet bør være minst %0% tegn og inneholde minst %1% numeriske tegn + %0% må være et heltall + %0% under %1% er obligatorisk + %0% er obligatorisk + %0% under %1% er ikke i et korrekt format + %0% er ikke i et korrekt format + + + Filtypen er deaktivert av administrator + NB! Selv om CodeMirror er aktivert i konfigurasjon er det deaktivert i Internet Explorer pga. ustabilitet. + Fyll ut både alias og navn på den nye egenskapstypen! + Det er et problem med lese/skrive rettighetene til en fil eller mappe + Tittel mangler + Type mangler + Du er i ferd med å gjøre bildet større enn originalen. Det vil forringe kvaliteten på bildet, ønsker du å fortsette? + Feil i python-skriptet + Python-skriptet ble ikke lagret fordi det inneholder en eller flere feil + Startnode er slettet. Kontakt din administrator + Du må markere innhold før du kan endre stil + Det er ingen aktive stiler eller formateringer på denne siden + Sett markøren til venstre i de 2 cellene du ønsker å slå sammen + Du kan ikke dele en celle som allerede er delt. + Feil i XSLT kode + XSLT ble ikke lagret på grunn av feil i koden + Det er et problem dem datatypen som brukes til denne egenskapen. Kontroller innstillingene og prøv igjen. + + + Om + Handling + Muligheter + Legg til + Alias + Er du sikker? + Ramme + av + Avbryt + Cellemargin + Velg + Lukk + Lukk vindu + Kommentar + Bekreft + Behold proposjoner + Fortsett + Kopier + Opprett + Database + Dato + Standard + Slett + Slettet + Sletter... + Design + Dimensjoner + Ned + Last ned + Rediger + Endret + Elementer + E-post + Feil + Finn + Høyde + Hjelp + Ikon + Importer + Indre margin + Sett inn + Installer + Justering + Språk + Layout + Laster + Låst + Logg inn + Logg ut + Logg ut + Makro + Flytt + Mer + Navn + Ny + Neste + Nei + av + OK + Åpne + eller + Passord + Sti + Plassholder ID + Ett øyeblikk... + Forrige + Egenskaper + E-post som innholdet i skjemaet skal sendes til + Papirkurv + Gjenværende + Gi nytt navn + Forny + Påkrevd + Prøv igjen + Rettigheter + Søk + Server + Vis + Hvilken side skal vises etter at skjemaet er sendt + Størrelse + Sorter + Submit + Type + Søk... + Opp + Oppdater + Oppgrader + Last opp + Url + Bruker + Brukernavn + Verdi + Visning + Velkommen... + Bredde + Ja + Mappe + Søkeresultater + + + Bakgrunnsfarge + Fet + Tekstfarge + Skrifttype + Tekst + + + Side + + + Installasjonsprogrammet kan ikke koble til databasen + Kunne ikke lagre Web.Config-filen. Vennligst endre databasens tilkoblingsstreng manuelt. + Din database er funnet og identifisert som + Databasekonfigurasjon + installer-knappen for å installere Umbraco %0% databasen]]> + Neste for å fortsette.]]> + Databasen ble ikke funnet! Vennligst sjekk at informasjonen i "connection string" i "web.config"-filen er korrekt.

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

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

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

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

    ]]>
    + Trykk Neste for å fortsette.]]> + neste for å fortsette konfigurasjonsveiviseren]]> + Passordet til standardbrukeren må endres!]]> + Standardbrukeren har blitt deaktivert eller har ingen tilgang til Umbraco!

    Ingen videre handling er nødvendig. Klikk neste for å fortsette.]]> + Passordet til standardbrukeren har blitt forandret etter installasjonen!

    Ingen videre handling er nødvendig. Klikk Neste for å fortsette.]]> + Passordet er blitt endret! + Umbraco skaper en standard bruker med login ( "admin") og passord ( "default") . Det er viktig at passordet er endret til noe unikt.

    Dette trinnet vil sjekke standard brukerens passord og foreslår hvis det må skiftes ]]> + Få en god start med våre introduksjonsvideoer + Ved å klikke på Neste-knappen (eller endre UmbracoConfigurationStatus i Web.config), godtar du lisensen for denne programvaren som angitt i boksen nedenfor. Legg merke til at denne Umbraco distribusjon består av to ulike lisenser, åpen kilde MIT lisens for rammen og Umbraco frivareverktøy lisens som dekker brukergrensesnittet. + Ikke installert. + Berørte filer og mapper + Mer informasjon om å sette opp rettigheter for Umbraco her + Du må gi ASP.NET brukeren rettigheter til å endre de følgende filer og mapper + Rettighetene er nesten perfekt satt opp!

    Du kan kjøre Umbraco uten problemer, men du vil ikke være i stand til å installere de anbefalte pakkene for å utnytte Umbraco fullt ut.]]> + Hvordan løse problemet + Klikk her for å lese tekstversjonen + innføringsvideo
    om å sette opp rettigheter for Umbraco eller les tekstversjonen.]]> + Rettighetsinnstillingene kan være et problem!


    Du kan kjøre Umbraco uten problemer, men du vil ikke være i stand til å installere de anbefalte pakkene for å utnytte Umbraco fullt ut.]]> + Rettighetsinstillingene er ikke klargjort for Umbraco!

    For å kunne kjøre Umbraco, må du oppdatere rettighetsinnstillingene dine.]]>
    + Rettighetsinnstillingene er perfekt!

    Du er klar for å kjøre Umbraco og installere pakker!]]>
    + Løser mappeproblem + Følg denne linken for mer informasjon om problemer med ASP.NET og oppretting av mapper + Konfigurerer mappetillatelser + + Jeg ønsker å starte fra bunnen. + lær hvordan) Du kan fortsatt velge å installere Runway senere. Vennligst gå til Utvikler-seksjonen og velg Pakker.]]> + Du har akkurat satt opp en ren Umbraco plattform. Hva vil du gjøre nå? + Runway er installert + Dette er vår liste av anbefalte moduler- Kryss av de du ønsker å installere, eller se denfulle listen av moduler ]]> + Bare anbefalt for erfarne brukere + Jeg vil starte med en enkel webside + "Runway" er en enkel webside som utstyrer deg med noen grunnleggende dokumenttyper og maler. Veiviseren kan sette opp Runway for deg automatisk, men du kan enkelt endre, utvide eller slette den. Runway er ikke nødvendig, og du kan enkelt bruke Umbraco uten den. Imidlertidig tilbyr Runway et enkelt fundament basert på de beste metodene for å hjelpe deg i gang fortere enn noensinne. Hvis du velger å installere Runway, kan du også velge blant grunnleggende byggeklosser kalt Runway Moduler for å forøke dine Runway-sider.

    Sider inkludert i Runway: Hjemmeside, Komme-i-gang, Installere moduler.
    Valgfrie Moduler: Toppnavigasjon, Sidekart, Kontakt, Galleri.
    ]]>
    + Hva er Runway + Steg 1/5 Godta lisens + Steg 2/5 Database konfigurasjon + Steg 3/5: Valider filrettigheter + Steg 4/5: Skjekk Umbraco sikkerheten + Steg 5/5: Umbraco er klar for deg til å starte! + Tusen takk for at du valgte Umbraco! + Se ditt nye nettsted Du har installert Runway, hvorfor ikke se hvordan ditt nettsted ser ut.]]> + Mer hjelp og info Få hjelp fra vårt prisbelønte samfunn, bla gjennom dokumentasjonen eller se noen gratis videoer på hvordan man bygger et enkelt nettsted, hvordan bruke pakker og en rask guide til Umbraco terminologi]]> + Umbraco %0% er installert og klar til bruk + web.config filen, og oppdatere AppSetting-nøkkelen UmbracoConfigurationStatus til verdien '%0%']]> + starte øyeblikkelig ved å klikke på "Start Umbraco" knappen nedenfor.
    Hvis du er ny på Umbraco, kan du finne mange ressurser på våre komme-i-gang sider.]]>
    + Start Umbraco For å administrere din webside, åpne Umbraco og begynn å legge til innhold, oppdatere maler og stilark eller utvide funksjonaliteten]]> + Tilkobling til databasen mislyktes. + Umbraco Versjon 3 + Umbraco Versjon 4 + Se + Umbraco %0% for en ny installasjon eller oppgradering fra versjon 3.0.

    Trykk "neste" for å starte veiviseren.]]>
    + + + Språkkode + Språk + + + Du har vært inaktiv og vil logges ut automatisk om + Forny innlogging for å lagre + + + Da er det søndag! + Smil, det er mandag! + Hurra, det er tirsdag! + For en herlig onsdag! + Gledelig torsdag! + Endelig fredag! + Gledelig lørdag + Logg på nedenfor + Din sesjon er utløpt + © 2001 - %0%
    umbraco.com

    ]]>
    + + + Skrivebord + Seksjoner + Innhold + + + Velg side over... + %0% er nå kopiert til %1% + Kopier til + %0% er nå flyttet til %1% + Flytt til + har blitt valgt som rot til ditt nye innhold, klikk 'ok' nedenfor. + Ingen node er valgt, vennligst velg en node i listen over før du klikker 'fortsett' + Gjeldende nodes type tillates ikke under valgt node + Gjeldende node kan ikke legges under en underordnet node + Denne noden kan ikke ligge på rotnivå + Handlingen tillates ikke. Du mangler tilgang til en eller flere underordnede noder. + Relater kopierte elementer til original(e) + + + Rediger dine varsler for %0% + + Hei %0%

    + +

    Dette er en automatisk mail for å informere om at handlingen '%1%' + er blitt utført på siden '%2%' + av brukeren '%3%' +

    + +

    +

    Rettelser:

    + + %6% +
    +

    + + + +

    Ha en fin dag!

    + Vennlig hilsen Umbraco roboten +

    ]]>
    + [%0%] Varsling om %1% utført på %2% + Varslinger + + + Umbraco-pakker har vanligvis endelsen ".umb" eller ".zip".]]> + Utvikler + Demonstrasjon + Dokumentasjon + Metadata + Pakkenavn + Pakken inneholder ingen elementer +
    Du kan trygt fjerne pakken fra systemet ved å klikke "avinstaller pakke" nedenfor.]]>
    + Ingen oppdateringer tilgjengelig + Alternativer for pakke + Lesmeg for pakke + Pakkebrønn + Bekreft avinstallering + Pakken ble avinstallert + Pakken ble vellykket avinstallert + Avinstaller pakke + Advarsel: alle dokumenter, media, etc. som som er avhengig av elementene du sletter, vil slutte å virke, noe som kan føre til ustabilitet, så avinstaller med forsiktighet. Hvis du er i tvil, kontakt pakkeutvikleren.]]> + Last ned oppdatering fra pakkeregisteret + Oppgrader pakke + Oppgraderingsinstrukser + Det er en oppdatering tilgjengelig for denne pakken. Du kan laste den ned direkte fra pakkebrønnen. + Pakkeversjon + Pakkeversjonshistorie + Se pakkens nettsted + + + Lim inn med full formattering (Anbefales ikke) + Teksten du er i ferd med å lime inn, inneholder spesialtegn eller formattering. Dette kan skyldes at du kopierer fra f.eks. Microsoft Word. Umbraco kan fjerne denne spesialformatteringen automatisk slik at innholdet er mer velegnet for visning på en webside. + Lim inn som ren tekst, dvs. fjern al formattering + Lim inn og fjern uegnet formatering (anbefalt) + + + Avansert: Beskytt ved å velge hvilke brukergrupper som har tilgang til siden + ved å bruke Umbraco's medlems-grupper]]> + rollebasert autentikasjon.]]> + Feilside + Brukt når personer logger på, men ikke har tilgang + Hvordan vil du beskytte siden din? + %0% er nå beskyttet + Beskyttelse fjernet fra %0% + Innloggingsside + Velg siden som har loginformularet + Fjern beskyttelse + Velg sidene som inneholder login-skjema og feilmelding ved feil innolgging. + Velg rollene som har tilgang til denne siden + Sett brukernavn og passord for denne siden + Enkelt: Beskytt ved hjelp av brukernavn og passord + Om du ønsker å bruke enkel autentisering via ett enkelt brukernavn og passord + + + %0% kunne ikke publiseres fordi den har planlagt utgivelsesdato. + %0% ble ikke publisert. Ett eller flere felter ble ikke godkjent av validering. + %0% kunne ikke publiseres fordi et tredjepartstillegg avbrøt handlingen. + %0% kan ikke publiseres fordi en overordnet side ikke er publisert. + Inkluder upubliserte undersider + Publiserer - vennligst vent... + %0% av %1% sider har blitt publisert... + %0% er nå publisert + %0% og alle undersider er nå publisert + Publiser alle undersider + ok for å publisere %0% og dermed gjøre innholdet synlig for alle.

    Du kan publisere denne siden og alle dens undersider ved å krysse av Publiser alle undersider nedenfor.]]>
    + + + Du har ikke konfigurert noen godkjente farger + + + skriv inn ekstern lenke + velg en intern side + Tittel + Lenke + Åpne i nytt vindu + Skriv inn en tekst + Skriv inn en lenke + + + Nullstill + + + Gjeldende versjon + Rød tekst vil ikke bli vist i den valgte versjonen. , grønn betyr lagt til]]> + Dokumentet er tilbakeført til en tidligere versjon + Dette viser den valgte versjonen som HTML, bruk avviksvisningen hvis du ønsker å se forksjellene mellom to versjoner samtidig. + Tilbakefør til + Velg versjon + Vis + + + Rediger scriptfilen + + + Concierge + Innhold + Courier + Utvikler + Umbraco konfigurasjonsveiviser + Mediaarkiv + Medlemmer + Nyhetsbrev + Innstillinger + Statistikk + Oversettelse + Brukere + Hjelp + Skjemaer + Analytics + + + gå til + Hjelpeemner for + Videokapitler for + De beste Umbraco opplæringsvideoer + + + Standardmal + Ordboksnøkkel + For å importere en dokumenttype, finn ".udt" filen på datamaskinen din ved å klikke "Utforsk" knappen og klikk "Importer" (du vil bli spurt om bekreftelse i det neste skjermbildet) + Ny tittel på arkfane + Nodetype + Type + Stilark + Script + Stilark-egenskap + Arkfane + Tittel på arkfane + Arkfaner + Hovedinnholdstype aktivert + Denne dokumenttypen bruker + som hoveddokumenttype. Arkfaner fra hoveddokumenttyper vises ikke og kan kun endres på hoveddokumenttypen selv. + Ingen egenskaper definert i denne arkfanen. Klikk på "legg til ny egenskap" lenken i toppen for å opprette en ny egenskap. + Hovedinnholdstype + Opprett tilhørende mal + + + Sort order + Creation date + Sortering ferdig. + Dra elementene opp eller ned for å arrangere dem. Du kan også klikke kolonneoverskriftene for å sortere alt på en gang. +
    Ikke lukk dette vinduet under sortering]]>
    + + + En feil oppsto + Utilstrekkelige brukertillatelser, kunne ikke fullføre operasjonen + Avbrutt + Handlingen ble avbrutt av et tredjepartstillegg + Publisering ble avbrutt av et tredjepartstillegg + Egenskaptypen finnes allerede + Egenskapstype opprettet + DataType: %1%]]> + Egenskapstype slettet + Innholdstype lagret + Du har opprettet en arkfane + Arkfane slettet + Arkfane med id: %0% slettet + Stilarket ble ikke lagret + Stilarket ble lagret + Stilark lagret uten feil + Datatype lagret + Ordbokelement lagret + Publiseringen feilet fordi den overliggende siden ikke er publisert + Innhold publisert + og er nå synlig for besøkende + Innhold lagret + Husk å publisere for å gjøre endringene synlig for besøkende + Sendt for godkjenning + Endringer har blitt sendt til godkjenning + Media lagret + Media lagret uten feil + Medlem lagret + Stilarksegenskap lagret + Stilark lagret + Mal lagret + Feil ved lagring av bruker (sjekk loggen) + Bruker lagret + Brukertypen lagret + Filen ble ikke lagret + Filen kunne ikke lagres. Vennligst sjekk filrettigheter + Filen ble lagret + Filen ble lagret uten feil + Språk lagret + Python-skriptet ble ikke lagret + Python-skriptet kunne ikke lagres fordi det inneholder en eller flere feil + Python-skriptet er lagret! + Ingen feil i python-skriptet! + Malen ble ikke lagret + Vennligst forviss deg om at du ikke har to maler med samme alias + Malen ble lagret + Malen ble lagret uten feil! + XSLT-koden ble ikke lagret + XSLT-koden inneholdt en feil + XSLT-koden ble ikke lagret, sjekk filrettigheter + XSLT lagret + Ingen feil i XSLT! + Innhold avpublisert + Delmal lagret + Delmal lagret uten feil + Delmal ble ikke lagret! + En feil oppsto ved lagring av delmal + Script visning lagret + Script visning lagret uten feil! + Script visning ikke lagret + En feil oppsto under lagring av filen. + En feil oppsto under lagring av filen. + + + Bruk CSS syntaks f.eks: h1, .redHeader, .blueText + Rediger stilark + Rediger egenskap for stilark + Navn for å identifisere stilarksegenskapen i rik-tekst editoren + Forhåndsvis + Stiler + + + Rediger mal + Sett inn innholdsområde + Sett inn plassholder for innholdsområde + Sett inn ordbokselement + Sett inn makro + Sett inn Umbraco sidefelt + Hovedmal + Hurtigguide til Umbraco sine maltagger + Mal + + + Sett inn element + Velg layout + Legg til rad + Legg til innhold + Slipp innhold + Raden har tilpasset design + + Innholdstypen er ikke tillatt her + Innholdstypen er tillatt her + + Klikk for å bygge inn + Klikk for å sette inn et bilde + Bildetekst... + Skriv her... + + Rutenettoppsett + Et oppsett er det overordnede arbeidsområdet til ditt rutenett - du vil typisk kun behøve ett eller to + Legg til rutenettoppsett + Juster oppsettet ved å konfigurere kolonnebredder og legge til ytterligere seksjoner + Radkonfigurasjoner + Rader er forhåndsdefinerte celler arrangert vannrett + Legg til radkonfigurasjon + Juster raden ved å sette cellebredder og legge til flere celler + + Kolonner + Totalt antall kolonner i rutenettet + + Innstillinger + Konfigurer hvilke innstillinger brukeren kan endre + + Stiler + Konfigurer hvilke stiler redaktørene kan endre + + Innstillingene lagres kun når konfigurasjonen er gyldig + + Tillatt alle editorer + Tillat alle radkonfigurasjoner + Bruk som standard + Velg ekstra + Velg standard + er lagt til + + + Alternativt felt + Alternativ tekst + Store/små bokstaver + Encoding + Felt som skal settes inn + Konverter linjeskift + Erstatter et linjeskift med htmltaggen <br> + Egendefinerte felt + Ja, kun dato + Formatter som dato + HTML koding + Formater spesialtegn med tilsvarende HTML-tegn. + Denne teksten vil settes inn etter verdien av feltet + Denne teksten vil settes inn før verdien av feltet + Små bokstaver + Ingen + Sett inn etter felt + Sett inn før felt + Rekursivt + Fjern paragraftagger + Fjerner eventuelle <P> rundt teksten + Standardfelter + Store bokstaver + URL koding + Dersom innholdet av feltene skal sendes til en URL skal spesialtegn formatteres + Denne teksten vil benyttes dersom feltene over er tomme + Dette feltet vil benyttes dersom feltet over er tomt + Ja, med klokkeslett. Dato/tid separator: + + + Oppgaver satt til deg + som du er tildelt. For å se en detaljert visning inkludert kommentarer, klikk på "Detaljer" eller navnet på siden. Du kan også laste ned siden som XML direkte ved å klikke på linken "Last ned XML".
    For å lukke en oversettelsesoppgave, vennligst gå til detaljvisningen og klikk på "Lukk" knappen.]]>
    + Lukk oppgave + Oversettelses detaljer + Last ned all oversettelsesoppgaver som XML + Last ned XML + Last ned XML DTD + Felt + Inkluder undersider + + [%0%] Oversettingsoppgave for %1% + Ingen oversettelses-bruker funnet. Vennligst opprett en oversettelses-bruker før du begynner å sende innhold til oversetting + Oppgaver opprettet av deg + opprettet av deg. For å se en detaljert visning inkludert kommentarer, klikk på "Detaljer" eller navnet på siden. Du kan også laste ned siden som XML direkte ved å klikke på linken "Last ned XML". For å lukke en oversettelsesoppgave, vennligst gå til detaljvisningen og klikk på "Lukk" knappen.]]> + Siden '%0%' har blitt sendt til oversetting + Send til oversetting + Tildelt av + Oppgave åpnet + Antall ord + Oversett til + Oversetting fullført. + Du kan forhåndsvise sidene du nettopp har oversatt ved å klikke nedenfor. Hvis den originale siden finnes, vil du få en sammenligning av sidene. + Oversetting mislykkes, XML filen kan være korrupt + Alternativer for oversetting + Oversetter + Last opp XML med oversettelse + + + Hurtigbufferleser + Papirkurv + Opprettede pakker + Datatyper + Ordbok + Installerte pakker + Installer utseende + Installer startpakke + Språk + Installer lokal pakke + Makroer + Mediatyper + Medlemmer + Medlemsgrupper + Roller + Medlemstyper + Dokumenttyper + Pakker + Pakker + Python Filer + Installer fra pakkeregister + Installer Runway + Runway moduler + Skriptfiler + Skript + Stiler + Maler + XSLT Filer + Analytics + + + Ny oppdatering er klar + %0% er klar, klikk her for å laste ned + Ingen forbindelse til server + Kunne ikke sjekke etter ny oppdatering. Se trace for mere info. + + + Administrator + Kategorifelt + Bytt passord + Nytt passord + Bekreft nytt passord + Du kan endre passordet til Umbraco ved å fylle ut skjemaet under og klikke "Bytt passord" knappen. + Innholdskanal + Beskrivelsesfelt + Deaktiver bruker + Dokumenttype + Redaktør + Utdragsfelt + Språk + Brukernavn + Øverste nivå i Media + Moduler + Deaktiver tilgang til Umbraco + Passord + Nullstill passord + Passordet er endret + Bekreft nytt passord + Nytt passord + Nytt passord kan ikke være blankt + Gjeldende passord + Feil passord + Nytt og bekreftet passord må være like + Nytt og bekreftet passord må være like + Overskriv tillatelser på undernoder + Du redigerer for øyeblikket tillatelser for sidene: + Velg sider for å redigere deres tillatelser + Søk i alle undersider + Startnode + Navn + Brukertillatelser + Brukertype + Brukertyper + Forfatter + Oversetter + Endre + Din profil + Din historikk + Sesjonen utløper om +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/js/install.loader.js b/src/Umbraco.Web.UI/Umbraco/js/install.loader.js deleted file mode 100644 index 869521ec7d..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/js/install.loader.js +++ /dev/null @@ -1,17 +0,0 @@ -LazyLoad.js( [ - 'lib/jquery/jquery.min.js', - /* 1.1.5 */ - 'lib/angular/1.1.5/angular.min.js', - 'lib/angular/1.1.5/angular-cookies.min.js', - 'lib/angular/1.1.5/angular-mobile.min.js', - 'lib/angular/1.1.5/angular-mocks.js', - 'lib/angular/1.1.5/angular-sanitize.min.js', - 'lib/underscore/underscore-min.js', - 'js/umbraco.installer.js', - 'js/umbraco.directives.js' - ], function () { - jQuery(document).ready(function () { - angular.bootstrap(document, ['ngSanitize', 'umbraco.install', 'umbraco.directives.validation']); - }); - } -); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Web.config b/src/Umbraco.Web.UI/Views/Web.config index c189e7dd93..479445713d 100644 --- a/src/Umbraco.Web.UI/Views/Web.config +++ b/src/Umbraco.Web.UI/Views/Web.config @@ -22,6 +22,7 @@ + diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 2f0bc30fad..2115c540b7 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - + diff --git a/src/Umbraco.Web.UI/config/ExamineIndex.config b/src/Umbraco.Web.UI/config/ExamineIndex.config index 7212053ca0..ab962e1dac 100644 --- a/src/Umbraco.Web.UI/config/ExamineIndex.config +++ b/src/Umbraco.Web.UI/config/ExamineIndex.config @@ -4,15 +4,15 @@ Umbraco examine is an extensible indexer and search engine. This configuration file can be extended to create your own index sets. Index/Search providers can be defined in the UmbracoSettings.config -More information and documentation can be found on CodePlex: http://umbracoexamine.codeplex.com +More information and documentation can be found on GitHub: https://github.com/Shazwazza/Examine/ --> - + - + @@ -25,6 +25,6 @@ More information and documentation can be found on CodePlex: http://umbracoexami - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/ExamineSettings.Release.config b/src/Umbraco.Web.UI/config/ExamineSettings.Release.config index 19b63dee7f..642785739f 100644 --- a/src/Umbraco.Web.UI/config/ExamineSettings.Release.config +++ b/src/Umbraco.Web.UI/config/ExamineSettings.Release.config @@ -4,7 +4,7 @@ Umbraco examine is an extensible indexer and search engine. This configuration file can be extended to add your own search/index providers. Index sets can be defined in the ExamineIndex.config if you're using the standard provider model. -More information and documentation can be found on CodePlex: http://umbracoexamine.codeplex.com +More information and documentation can be found on GitHub: https://github.com/Shazwazza/Examine/ --> diff --git a/src/Umbraco.Web.UI/config/ExamineSettings.config b/src/Umbraco.Web.UI/config/ExamineSettings.config index 6759b5a21d..aacf5e3732 100644 --- a/src/Umbraco.Web.UI/config/ExamineSettings.config +++ b/src/Umbraco.Web.UI/config/ExamineSettings.config @@ -4,14 +4,14 @@ Umbraco examine is an extensible indexer and search engine. This configuration file can be extended to add your own search/index providers. Index sets can be defined in the ExamineIndex.config if you're using the standard provider model. -More information and documentation can be found on CodePlex: http://umbracoexamine.codeplex.com +More information and documentation can be found on GitHub: https://github.com/Shazwazza/Examine/ --> - + @@ -22,7 +22,7 @@ More information and documentation can be found on CodePlex: http://umbracoexami useTempStorage="Sync"/> - @@ -31,15 +31,15 @@ More information and documentation can be found on CodePlex: http://umbracoexami - diff --git a/src/Umbraco.Web.UI/config/log4net.Release.config b/src/Umbraco.Web.UI/config/log4net.Release.config index bbca2a8277..10b8c9eb7a 100644 --- a/src/Umbraco.Web.UI/config/log4net.Release.config +++ b/src/Umbraco.Web.UI/config/log4net.Release.config @@ -7,7 +7,7 @@ - + diff --git a/src/Umbraco.Web.UI/config/log4net.config b/src/Umbraco.Web.UI/config/log4net.config index 497fd4471f..eca84962a1 100644 --- a/src/Umbraco.Web.UI/config/log4net.config +++ b/src/Umbraco.Web.UI/config/log4net.config @@ -7,7 +7,7 @@ - + diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index 625ee5a653..5e7f289d2f 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -16,15 +16,8 @@ - - - - - diff --git a/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config b/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config index 3b0681b009..a18841983d 100644 --- a/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config +++ b/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config @@ -2,220 +2,246 @@ - - - code - images/editor/code.gif - code - 1 - - + + + code + Code + images/editor/code.gif + code + 1 + + codemirror + Code mirror images/editor/code.gif codemirror 1 - removeformat - images/editor/removeformat.gif - removeformat - 2 - - - - Undo - images/editor/undo.gif - undo - 11 - - - Redo - images/editor/redo.gif - redo - 12 - - - Cut - images/editor/cut.gif - cut - 13 - - - Copy - images/editor/copy.gif - copy - 14 - - - - styleselect - images/editor/showStyles.png - styleselect - 20 - - - bold - images/editor/bold.gif - bold - 21 - - - italic - images/editor/italic.gif - italic - 22 - - - Underline - images/editor/underline.gif - underline - 23 - - - Strikethrough - images/editor/strikethrough.gif - strikethrough - 24 - - - - JustifyLeft - images/editor/justifyleft.gif - justifyleft - 31 - - - JustifyCenter - images/editor/justifycenter.gif - justifycenter - 32 - - - JustifyRight - images/editor/justifyright.gif - justifyright - 33 - - - JustifyFull - images/editor/justifyfull.gif - alignjustify - 34 - - - - bullist - images/editor/bullist.gif - bullist - 41 - - - numlist - images/editor/numlist.gif - numlist - 42 - - - Outdent - images/editor/outdent.gif - outdent - 43 - - - Indent - images/editor/indent.gif - indent - 44 - - - - mceLink - images/editor/link.gif - link - 51 - - - unlink - images/editor/unLink.gif - unlink - 52 - - - mceInsertAnchor - images/editor/anchor.gif - anchor - 53 - - - - mceImage - images/editor/image.gif - image - 61 - - - - umbracomacro - images/editor/insMacro.gif - umbracomacro - 62 - - - - - - mceInsertTable - images/editor/table.gif - table - 63 - - - - umbracoembed - images/editor/media.gif - umbracoembed - 66 - - - inserthorizontalrule - images/editor/hr.gif - hr - 71 - - - subscript - images/editor/sub.gif - subscript - 72 - - - - superscript - images/editor/sup.gif - superscript - 73 - - - - mceCharMap - images/editor/charmap.gif - charmap - 74 - - - - + removeformat + Remove format + images/editor/removeformat.gif + removeformat + 2 + + + undo + Undo + Remove Format + images/editor/undo.gif + undo + 11 + + + redo + Redo + images/editor/redo.gif + redo + 12 + + + cut + Cut + images/editor/cut.gif + cut + 13 + + + copy + Copy + images/editor/copy.gif + copy + 14 + + + paste + Paste + images/editor/paste.gif + paste + 15 + + + styleselect + Style select + images/editor/showStyles.png + styleselect + 20 + + + bold + Bold + images/editor/bold.gif + bold + 21 + + + italic + Italic + images/editor/italic.gif + italic + 22 + + + underline + Underline + images/editor/underline.gif + underline + 23 + + + strikethrough + Strikethrough + images/editor/strikethrough.gif + strikethrough + 24 + + + justifyleft + Justify left + images/editor/justifyleft.gif + justifyleft + 31 + + + justifycenter + Justify center + images/editor/justifycenter.gif + justifycenter + 32 + + + justifyright + Justify right + images/editor/justifyright.gif + justifyright + 33 + + + justifyfull + Justify full + images/editor/justifyfull.gif + alignjustify + 34 + + + bullist + Bullet list + images/editor/bullist.gif + bullist + 41 + + + numlist + Numbered list + images/editor/numlist.gif + numlist + 42 + + + outdent + Decrease indent + images/editor/outdent.gif + outdent + 43 + + + indent + Increase indent + images/editor/indent.gif + indent + 44 + + + mceLink + Insert/edit link + images/editor/link.gif + link + 51 + + + unlink + Remove link + images/editor/unLink.gif + unlink + 52 + + + mceInsertAnchor + Anchor + images/editor/anchor.gif + anchor + 53 + + + mceImage + Image + images/editor/image.gif + image + 61 + + + umbracomacro + Macro + images/editor/insMacro.gif + umbracomacro + 62 + + + mceInsertTable + Table + images/editor/table.gif + table + 63 + + + umbracoembed + Embed + images/editor/media.gif + umbracoembed + 66 + + + inserthorizontalrule + Horizontal rule + images/editor/hr.gif + hr + 71 + + + subscript + Subscript + images/editor/sub.gif + subscript + 72 + + + superscript + Superscript + images/editor/sup.gif + superscript + 73 + + + mceCharMap + Character map + images/editor/charmap.gif + charmap + 74 + + + code - codemirror - paste - umbracolink - anchor - charmap - table + codemirror + paste + umbracolink + anchor + charmap + table lists - - - hr + + + - - font + + font - - - - raw - - { - "indentOnInit": false, - "path": "../../../../lib/codemirror", - "config": { - }, - "jsFiles": [ - ], - "cssFiles": [ - ] - } - - + + + + raw + + { + "indentOnInit": false, + "path": "../../../../lib/codemirror", + "config": { + }, + "jsFiles": [ + ], + "cssFiles": [ + ] + } + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/tinyMceConfig.config b/src/Umbraco.Web.UI/config/tinyMceConfig.config index dc41e90908..0e107deca5 100644 --- a/src/Umbraco.Web.UI/config/tinyMceConfig.config +++ b/src/Umbraco.Web.UI/config/tinyMceConfig.config @@ -5,214 +5,238 @@ code + Code images/editor/code.gif code 1 codemirror + Code mirror images/editor/code.gif codemirror 1 removeformat + Remove format images/editor/removeformat.gif removeformat 2 - - Undo + undo + Undo images/editor/undo.gif undo 11 - Redo + redo + Redo images/editor/redo.gif redo 12 - Cut + cut + Cut images/editor/cut.gif cut 13 - Copy + copy + Copy images/editor/copy.gif copy 14 - + + paste + Paste + images/editor/paste.gif + paste + 15 + styleselect + Style select images/editor/showStyles.png styleselect 20 bold + Bold images/editor/bold.gif bold 21 italic + Italic images/editor/italic.gif italic 22 - Underline + underline + Underline images/editor/underline.gif underline 23 - Strikethrough + strikethrough + Strikethrough images/editor/strikethrough.gif strikethrough 24 - - JustifyLeft + justifyleft + Justify left images/editor/justifyleft.gif justifyleft 31 - JustifyCenter + justifycenter + Justify center images/editor/justifycenter.gif justifycenter 32 - JustifyRight + justifyright + Justify right images/editor/justifyright.gif justifyright 33 - JustifyFull + justifyfull + Justify full images/editor/justifyfull.gif alignjustify 34 - bullist + Bullet list images/editor/bullist.gif bullist 41 numlist + Numbered list images/editor/numlist.gif numlist 42 - Outdent + outdent + Decrease indent images/editor/outdent.gif outdent 43 - Indent + indent + Increase indent images/editor/indent.gif indent 44 - mceLink + Insert/edit link images/editor/link.gif link 51 unlink + Remove link images/editor/unLink.gif unlink 52 mceInsertAnchor + Anchor images/editor/anchor.gif anchor 53 - mceImage + Image images/editor/image.gif image 61 - umbracomacro + Macro images/editor/insMacro.gif umbracomacro 62 - - - mceInsertTable + Table images/editor/table.gif table 63 - umbracoembed + Embed images/editor/media.gif umbracoembed 66 inserthorizontalrule + Horizontal rule images/editor/hr.gif hr 71 subscript + Subscript images/editor/sub.gif subscript 72 - superscript + Superscript images/editor/sup.gif superscript 73 - mceCharMap + Character map images/editor/charmap.gif charmap 74 - - + code codemirror paste - umbracolink anchor charmap table lists + hr { "indentOnInit": false, - "path": "../../../../lib/codemirror", + "path": "../../../../lib/codemirror", "config": { }, "jsFiles": [ diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index bb60356cd4..9e61db62f3 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -3,39 +3,47 @@ + + - + + + + - - - - + - - + + + - + + + - - + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 8a85f26788..91c216c727 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -7,17 +7,17 @@ - - - + + - - + + + - - + + - + @@ -30,12 +30,16 @@ - - + + + iconClosed="icon-folder" iconOpen="icon-folder" sortOrder="10" />--> + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 5a8af019f0..dd47cd5570 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -37,6 +37,8 @@ --> 1 + 1079 + 1080 @@ -102,6 +104,7 @@ Textstring + @@ -111,6 +114,9 @@ false + + true + diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index efab0db7da..160a82c5fb 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -5,8 +5,8 @@ - - + + @@ -18,8 +18,9 @@ - - + + + @@ -27,10 +28,13 @@ - + - - + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/ClientRedirect.aspx b/src/Umbraco.Web.UI/umbraco/ClientRedirect.aspx index 72232c99cd..2a10d4d344 100644 --- a/src/Umbraco.Web.UI/umbraco/ClientRedirect.aspx +++ b/src/Umbraco.Web.UI/umbraco/ClientRedirect.aspx @@ -10,7 +10,7 @@ - + @*And finally we can load in our angular app*@ @@ -86,5 +87,12 @@ @Html.RenderProfiler() } + + + diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml index 0e13eba345..9fca6d082a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.Release.xml @@ -1,21 +1,5 @@ - - -
    Nodetype
    - /create/nodeType.ascx - - - - -
    - -
    Nodetype
    - /create/nodeType.ascx - - - - -
    +
    Template
    /create/simple.ascx @@ -73,28 +57,7 @@ -
    - -
    Datatype
    - /create/simple.ascx - - - -
    - -
    Datatype
    - /create/simple.ascx - - - -
    - -
    membertype
    - /create/simple.ascx - - - -
    +
    membergroup
    /create/simple.ascx @@ -115,78 +78,28 @@ -
    - -
    mediatype
    - /create/simple.ascx +
    + +
    member
    + /create/member.ascx - +
    - -
    Medie type
    - /create/simple.ascx + +
    member
    + /create/member.ascx - - +
    - -
    member
    - /create/member.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    membertype
    - /create/member.ascx - - - -
    - +
    membergroup
    /create/simple.ascx -
    - - +
    Stylesheet editor egenskab
    /create/simple.ascx diff --git a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml index 1ab0f4180c..f6859c6423 100644 --- a/src/Umbraco.Web.UI/umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/umbraco/config/create/UI.xml @@ -1,21 +1,5 @@  - - -
    Nodetype
    - /create/nodeType.ascx - - - - -
    - -
    Nodetype
    - /create/nodeType.ascx - - - - -
    +
    Template
    /create/simple.ascx @@ -73,28 +57,7 @@ -
    - -
    Datatype
    - /create/simple.ascx - - - -
    - -
    Datatype
    - /create/simple.ascx - - - -
    - -
    membertype
    - /create/simple.ascx - - - -
    +
    membergroup
    /create/simple.ascx @@ -115,78 +78,14 @@ -
    - -
    mediatype
    - /create/simple.ascx - - - -
    - -
    Medie type
    - /create/simple.ascx - - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    membertype
    - /create/member.ascx - - - -
    - + +
    membergroup
    /create/simple.ascx
    - -
    Stylesheet editor egenskab
    /create/simple.ascx diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index cfcfca042a..0280a46e05 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -60,6 +60,7 @@ For + Ryd valg Vælg Vælg nuværende mappe Gør noget andet @@ -84,11 +85,13 @@ Gem Gem og udgiv Gem og send til udgivelse + Gem listevisning Se siden Preview er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder Indsæt tabel + Generer modeller For at skifte det valgte indholds dokumenttype, skal du først vælge en ny dokumenttype, som er gyldig på denne placering. @@ -163,6 +166,10 @@ Klik for at uploade Slip filerne her... + Link til medie + eller klik her for at vælge filer + Tilladte filtyper er kun + Maks filstørrelse er Opret et nyt medlem @@ -174,6 +181,7 @@ Vælg en type og skriv en titel "dokument typer".]]> "media typer".]]> + Dokument type uden skabelon Til dit website @@ -184,6 +192,12 @@ Besøg Velkommen + + Bliv + Kassér ændringer + Du har ikke-gemte ændringer + Er du sikker på du vil navigere væk fra denne side? - du har ikke-gemte ændringer + Navn på lokalt link Rediger domæner @@ -194,7 +208,7 @@ Er du sikker på at du vil forlade Umbraco? Er du sikker? Klip - Rediger ordbogs nøgle + Rediger ordbogsnøgle Rediger sprog Indsæt lokalt link Indsæt tegn @@ -239,6 +253,8 @@ Indtast dit kodeord Navngiv %0%... Indtast navn... + Label... + Indtast beskrivelse Søg... Filtrer... Indtast nøgleord (tryk på Enter efter hvert nøgleord)... @@ -288,7 +304,8 @@ %0% er ikke i et korrekt format - Denne filttype er blevet deaktiveret af administratoren + Der skete en fejl på severen + Denne filttype er blevet deaktiveret af administratoren OBS! Selvom CodeMirror er slået til i konfigurationen, så er den deaktiveret i Internet Explorer fordi den ikke er stabil nok. Du skal udfylde både Alias & Navn på den nye egenskabstype! Der mangler læse/skrive rettigheder til bestemte filer og mapper @@ -313,6 +330,7 @@ Tilføj Alias Er du sikker? + Tilbage Kant af Fortryd @@ -376,6 +394,7 @@ Egenskaber Email der skal modtage indhold af formular Papirkurv + Din papirkurv er tom Mangler Omdøb Forny @@ -388,6 +407,7 @@ Hvilken side skal vises efter at formularen er sendt Størrelse Sortér + Indsend Type Skriv for at søge... Op @@ -404,7 +424,46 @@ Ja Mappe Søgeresultater + Sorter + Afslut sortering + Eksempel + Skift kodeord + til + Listevisning + Gemmer... + nuværende + Indlejring + valgt + + + Sort + Grøn + Gul + Orange + Blå + Rød + + + + Tilføj fane + Tilføj property + Tilføj editor + Tilføj skabelon + Tilføj child node + Tilføj child + + Rediger data type + + Naviger sektioner + + Genveje + Vis genveje + + Brug listevisning + Tillad på rodniveau + + Baggrundsfarve Fed @@ -491,14 +550,14 @@ - Så er det søndag! + Så er det søndag! Smil, det er mandag! Hurra, det er tirsdag! Hvilken herlig onsdag! Glædelig torsdag! Endelig fredag! Glædelig lørdag - + indtast brugernavn og kodeord Din session er udløbet @@ -534,23 +593,23 @@ Hej %0% Dette er en automatisk mail for at fortælle at handlingen '%1%' er blevet udført på siden '%2%' af brugeren '%3%' - + Gå til http://%4%/#/content/content/edit/%5% for at redigere. - + Ha' en dejlig dag! - + Mange hilsner fra Umbraco robotten ]]> - Hej %0%

    + Hej %0%

    Dette er en automatisk mail for at informere dig om at opgaven '%1%' er blevet udførtpå siden '%2%' af brugeren '%3%'

    -

    +

    Opdateringssammendrag:

    %6%

    Hav en fortsat god dag!

    De bedste hilsner fra umbraco robotten

    ]]> [%0%] Notificering om %1% udført på %2% Notificeringer - Vælg hvor den lokale pakke skal placeres + Vælg pakken fra din computer. Umbraco pakker er oftest en ".zip" fil Udvikler Demonstration Dokumentation @@ -625,9 +684,15 @@ Mange hilsner fra Umbraco robotten Nulstil - Indsæt element - Tilføj rækker til dit layout - nedenfor og tilføje det første element]]> + Vælg indholdstype + Vælg layout + Tilføj række + Tilføj indhold + Slip indhold + Instillinger tilføjet + + Indholdet er ikke tilladt her + Indholdet er tilladt her Klik for at indlejre Klik for at indsætte et billede @@ -658,7 +723,69 @@ Mange hilsner fra Umbraco robotten Tillad alle editorer Tillad alle rækkekonfigurationer + + Sæt som standard + Vælg ekstra + Vælg standard + er tilføjet + + + + Kompositioner + Du har ikke tilføjet nogle faner + Tilføj ny fane + Tilføj endnu en fane + Nedarvet fra + Tilføj property + Påkrævet label + + Aktiver listevisning + Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet + + Tilladte skabeloner + Vælg hvilke skabeloner der er tilladt at bruge på dette indhold + + Tillad på rodniveau + Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv + Ja – indhold af denne type er tilladt i roden + + Tilladte typer + Tillad at oprette indhold af en specifik type under denne + + Vælg child node + + Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. + Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition + Der er ingen indholdstyper tilgængelige at bruge som komposition + + Tilgængelige editors + Genbrug + Editor indstillinger + + Konfiguration + + Ja, slet + + blev flyttet til + Vælg hvor + skal flyttes til + + Alle dokumenttyper + Alle dokumenter + Alle medier + + som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. + + og alle dokumenter, som benytter denne type + og alle medier, som benytter denne type + og alle medlemmer, som benytter denne type + + der bruger denne editor vil blive opdateret med de nye indstillinger + + Nuværende version Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> @@ -711,6 +838,8 @@ Mange hilsner fra Umbraco robotten Opret matchende skabelon + Sorteringsrækkefølge + Oprettelsesdato Sortering udført Træk de forskellige sider op eller ned for at indstille hvordan de skal arrangeres, eller klik på kolonnehovederne for at sortere hele rækken af sider
    Luk ikke dette vindue imens]]>
    @@ -753,6 +882,8 @@ Mange hilsner fra Umbraco robotten Fil gemt Fil gemt uden fejl Sprog gemt + Medietype gemt + Medlemstype gemt Python script ikke gemt Python-script kunne ikke gemmes pga. fejl Python-script gemt @@ -851,7 +982,7 @@ Mange hilsner fra Umbraco robotten Cacheviser Papirkurv Oprettede pakker - Datatyper + Datatyper Ordbog Installerede pakker Installér et skin' @@ -861,10 +992,12 @@ Mange hilsner fra Umbraco robotten Makroer Medietyper Medlemmer - Medlemsgruppe + Medlemsgruppe Roller - Medlemstype - Dokumenttyper + Medlemstype + Dokumenttyper + Relationstyper + Pakker Pakker Python @@ -872,7 +1005,7 @@ Mange hilsner fra Umbraco robotten Installer Runway Runway-moduler Scripting filer - Script + Scripts Stylesheets Skabeloner XSLT-filer @@ -888,7 +1021,7 @@ Mange hilsner fra Umbraco robotten Administrator Kategorifelt Skift dit kodeord - Skift dit kodeord + Nyt kodeord Gentag dit nye kodeord Du kan ændre dit kodeord, som giver dig adgang til Umbraco Back Office ved at udfylde formularen og klikke på knappen 'Skift dit kodeord' Indholdskanal @@ -902,6 +1035,7 @@ Mange hilsner fra Umbraco robotten Startnode i mediearkivet Moduler Deaktivér adgang til Umbraco + Gammelt kodeord Adgangskode Nulstil kodeord Dit kodeord er blevet ændret! @@ -923,8 +1057,17 @@ Mange hilsner fra Umbraco robotten Brugertyper Forfatter Oversætter + Skift Din profil Din historik Session udløber + + Validation + Valider som email + Valider som tal + Valider som Url + ...eller indtast din egen validering + Feltet er påkrævet + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml index c6f87964e7..3b4c1ee5ab 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml @@ -1,5 +1,5 @@ - - + + The Umbraco community http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files @@ -8,7 +8,7 @@ Hostnamen verwalten Protokoll Durchsuchen - Dokumententyp ändern + Dokumenttyp ändern Kopieren Erstellen Paket erstellen @@ -24,9 +24,10 @@ Benachrichtigungen Öffentlicher Zugriff Veröffentlichen - Veröffentlichung zurück nehmen + Veröffentlichung zurücknehmen Aktualisieren Erneut veröffentlichen + Wiederherstellen Berechtigungen Zurücksetzen Zur Veröffentlichung einreichen @@ -35,15 +36,15 @@ Zur Veröffentlichung einreichen Übersetzen Aktualisieren - Standard Wert + Standardwert - Zugriff verweigert + Erlaubnis verweigert. Neue Domain hinzufügen - Entfernen - Ungültiges Element - Format der Domain ungültig - Die Domain ist bereits zugeordnet + entfernen + Ungültiges Element. + Format der Domain ungültig. + Domain wurde bereits zugewiesen. Sprache Domain Domain '%0%' hinzugefügt @@ -51,23 +52,20 @@ Die Domain '%0%' ist bereits zugeordnet Domain '%0%' aktualisiert Domains bearbeiten -
    1-Level Pfade in Domains werden unterstützt, z.B. "example.com/en". Diese sollten aber nach Möglichkeit vermieden werden. Besser sollten - die Sprachkultur-Einstellungen verwendet werden.]]>
    + Beispiel: example.com, www.example.com Vererben - Sprachkultur - Definiert die Sprachkultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. - - Domains - + Kultur + Definiert die Kultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. + Domains + Ansicht für Auswählen - Verzeichnis wählen - Etwas anders machen - Fett + Aktuellen Ordner auswählen + Etwas anderes machen + Fett Ausrücken Formularelement einfügen Graphische Überschrift einfügen @@ -82,37 +80,37 @@ Aufzählung Nummerierung Makro einfügen - Bild einfügen + Abbildung einfügen Datenbeziehungen bearbeiten - Zürück zur Übersicht + Zurück zur Liste Speichern Speichern und veröffentlichen Speichern und zur Abnahme übergeben Vorschau - Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist + Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist Stil auswählen Stil anzeigen Tabelle einfügen - Um einen Dokumententyp zu ändern muss zunächst ein gültiger Dokumententyp in der Liste für diesen Inhalt ausgewählt werden. - Danach muss die Zuweisung der Eigenschaften vom aktuellen zum neuen Dokumententyp bestätigt und/oder geändert werden und auf Speichern geklickt werden. + Um den Typ des ausgewählten Dokuments zu ändern, wählen Sie bitte zunächst aus der Liste der an dieser Stelle erlaubten Dokumenttypen. + Im Anschluss bestätigen oder korrigieren Sie die Zuordnung der Eigenschaften und klicken Sie auf 'Speichern'. Der Inhalt wurde neu veröffentlicht. - Aktuelle Eigenschaft - Aktueller Doumententyp - Der Dokumententyp kann nicht geändert werden, da es keine alternativen gültigen Eigenschaften für dieses Dokument gibt. Eine Alternative wird gültig sein, wenn es unter dem übergeordenten Knoten des ausgewählten Dokuments erlaubt ist und alle bestehenden untergeordneten Dokumente unter diesem erstellt werden dürfen. - Dokumententyp ändern - Eigenschaften zuweisen - Zu Eigenschaft zuweisen + Derzeitige Eigenschaft + Derzeitiger Datentyp + Der Typ dieses Dokuments kann nicht geändert werden, da an dieser Stelle keine Alternativen zugelassen sind. Ein alternativer Dokumenttyp kann nur dann verwendet werden, wenn er unterhalb des diesem Dokument übergeordneten Elements angelegt werden darf. + Dokumenttyp geändert + Eigenschaften zuordnen + Dieser Eigenschaft zuordnen Neue Vorlage - Neue Eigenschaft - keine + Neuer Typ + keiner Inhalt - Neuen Dokumententyp auswählen - Der Dokumententyp des ausgewählten Dokuments wurde erfolgreich geändert zu [new type] und zu folgenden Eigenschaften zugewiesen: - zu - Zuweisung der Eigenschaft konnte nicht abgeschlossen werden, weil es eine oder mehr Eigenschaften mehr gibt als für eine Zuweisung definiert wurden. - Nur wecheselnde Eigenschaften sind gültig für das aktuell angezeigte Dokument. + Neuen Dokumenttyp auswählen + Der Typ des ausgewählten Dokuments wurde erfolgreich zu [new type] geändert und die Eigenschaften wie folgend zugeordnet: + nach + Die Zuordnung der Eigenschaften kann nicht abgeschlossen werden, da mindestens eine Eigenschaft mehrfach zugeordnet werden soll. + Nur an dieser Stelle erlaubte Dokumenttypen werden angezeigt. Ist veröffentlicht @@ -122,19 +120,19 @@ Alternative Links Klicken, um das Dokument zu bearbeiten Erstellt von - Ursprünglicher Author - Geändert von + Ursprünglicher Autor + Aktualisiert von Erstellt am - Erstellungszeitpunkt des Dokuments + Erstellungszeitpunkt des Dokuments Dokumenttyp In Bearbeitung Veröffentlichung aufheben am Dieses Dokument wurde nach dem Veröffentlichen bearbeitet. Dieses Dokument ist nicht veröffentlicht. Zuletzt veröffentlicht - Es gibt keine anzeigbaren Elemente in dieser Liste. + Diese Liste enthält keine Einträge. Medientyp - Link zu Medienobjekt(en) + Verweis auf Medienobjekt(e) Mitgliedergruppe Mitgliederrolle Mitglieder-Typ @@ -142,38 +140,42 @@ Name des Dokument Eigenschaften Dieses Dokument ist veröffentlicht aber nicht sichtbar, da das übergeordnete Dokument '%0%' nicht publiziert ist - Ups! Dieses Dokument ist veröffentlicht, aber befindet sich nicht im internen Cache (Systemfehler) + Ups! Dieses Dokument ist veröffentlicht aber nicht im internen Cache aufzufinden: Systemfehler. Veröffentlichen Publikationsstatus Veröffentlichen am - Veröffentlichung aufheben am + Veröffentlichung widerrufen am Datum entfernen Sortierung abgeschlossen Um die Dokumente zu sortieren, ziehen Sie sie einfach an die gewünschte Position. Sie können mehrere Zeilen markieren indem Sie die Umschalttaste ("Shift") oder die Steuerungstaste ("Strg") gedrückt halten Statistiken Titel (optional) - Alternativer Text (optional) + Alternativtext (optional) Typ - Veröffentlichung zurück nehmen + Veröffentlichung widerrufen Zuletzt bearbeitet am Letzter Änderungszeitpunkt des Dokuments Datei entfernen Link zum Dokument Mitglied der Gruppe(n) Kein Mitglied der Gruppe(n) - Untergeordnete Elemente - Ziel + Untergeordnete Elemente + Ziel - - Klicken zum Hochladen - Dateien hierher ziehen... + + Für Upload klicken + Dateien hier fallen lassen ... + + + Neues Mitglied anlegen + Alle Mitglieder An welcher Stellen wollen Sie das Element erstellen Erstellen unter Wählen Sie einen Namen und einen Typ - Es stehen keine erlaubten Dokumenttypen zur Verfügung. Sie müssen diese in den Einstellungen (unter "Dokumenttypen") aktivieren. - Es stehen keine erlaubten Medientypen zur Verfügung. Sie müssen diese in den Einstellungen (unter "Medientypen") aktivieren. + Es stehen keine erlaubten Dokumenttypen zur Verfügung. Sie müssen diese in den Einstellungen (unter "Dokumenttypen") aktivieren. + Es stehen keine erlaubten Medientypen zur Verfügung. Sie müssen diese in den Einstellungen (unter "Medientypen") aktivieren. Website anzeigen @@ -184,6 +186,12 @@ Besuchen Willkommen + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Name Hostnamen verwalten @@ -232,24 +240,22 @@ Bearbeiten Sie nachfolgend die verschiedenen Sprachversionen für den Wörterbucheintrag '<em>%0%</em>'. <br/>Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen. - Name der Sprachkultur - - - - -Benutzername eingeben - Passwort eingeben - Benenne %0%... - Name eingeben... - Suchbegriff eingeben... - Tippen zum Filtern... - Tippen für Schlagworte hinzuzufügen (drücke Enter nach jedem Schlagwort)... + Name der Kultur + + + Benutzername eingeben + Kennwort eingeben + %0% benennen ... + Name angeben ... + Durchsuchen ... + Filtern ... + Tippen, um Tags hinzuzufügen (nach jedem Tag die Eingabetaste drücken) ... - Als Wurzel-Knoten erlauben - Nur Dokumententype mit dieser Markierung können auf Ebene 1 im Inhalts- und Medienverzeichnisbaum erstellt werden + Auf oberster Ebene erlauben + Nur diese Dokumenttypen können auf oberster Ebene in Inhalte und Medien angelegt werden Dokumenttypen, die unterhalb dieses Typs erlaubt sind - Dokumententyp-Kompositionen + Zusammengesetzte Dokumenttypen Erstellen Registerkarte löschen Beschreibung @@ -257,11 +263,11 @@ Registerkarte Illustration Listenansicht aktivieren - Konfiguriert das Dokument zur Darstellung einer sortierbaren und durchsuchbaren Liste mit untergeordneten Dokumenten. Die untergeordneten Elemente werden im Verzeichnisbaum nicht mehr angezeigt. + Aktiviert eine durchsuch- und sortierbare Listendarstellung der untergeordneten Elemente anstelle diese in der Baumstruktur anzuzeigen Aktuelle Listenansicht - Datentyp der aktiven Listenansicht - Erstelle benutzerdefinierte Listenansicht - Entferne benutzerdefinierte Listenansicht + Der Datentyp für die aktuelle Ansicht der Liste + Angepasste Listenansicht erstellen + Angepasste Listenansicht entfernen Vorgabewert hinzufügen @@ -290,6 +296,7 @@ '%0%' hat ein falsches Format + Dieser Dateityp wird durch die Systemeinstellungen blockiert ACHTUNG! Obwohl CodeMirror in den Einstellungen aktiviert ist, bleibt das Modul wegen mangelnder Stabilität in Internet Explorer deaktiviert. Bitte geben Sie die Bezeichnung und den Alias des neuen Dokumenttyps ein. Es besteht ein Problem mit den Lese-/Schreibrechten auf eine Datei oder einen Ordner @@ -305,7 +312,7 @@ Sie können keine Zelle trennen, die nicht zuvor aus mehreren zusammengeführt wurde. Fehler im XSLT Das XSLT ist fehlerhaft und wurde daher nicht gespeichert. - Dieser Dateityp wird durch die Systemeinstellungen blockiert + Es liegt ein Konfigurationsfehler beim Datentyp dieser Eigenschaft vor. Bitte prüfen Sie den Datentyp bzw. die Eigenschaft. Info @@ -380,7 +387,7 @@ Verbleibend Umbenennen Erneuern - Pflichtangaben + Pflichtangabe Wiederholen Berechtigungen Suchen @@ -389,8 +396,9 @@ Seite beim Senden anzeigen Größe Sortieren + Submit Typ - nach Inhalten suchen ... + Durchsuchen ... nach oben Aktualisieren Update @@ -405,10 +413,9 @@ Ja Ordner Suchergebnisse + Reorder + I am done reordering - - - Hintergrundfarbe Fett @@ -438,7 +445,7 @@ Keine Sorge - Dabei werden keine Inhalte gelöscht und alles wird weiterhin funktionieren! </p> -Die Datenbank wurde auf die Version %0% aktualisiert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. + Die Datenbank wurde auf die Version %0% aktualisiert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. Die Datenbank ist fertig eingerichtet. Klicken Sie auf <strong>"weiter"</strong>, um mit der Einrichtung fortzufahren. <strong>Das Kennwort des Standard-Benutzers muss geändert werden!</strong> <strong>Der Standard-Benutzer wurde deaktiviert oder hat keinen Zugriff auf Umbraco.</strong></p><p>Es sind keine weiteren Aktionen notwendig. Klicken Sie auf <b>Weiter</b> um fortzufahren. @@ -514,8 +521,8 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Dieser Assistent führt Sie durch die Einrichtung einer neuen Installation von <strong>Umbraco %0%</strong> oder einem Upgrade von Version 3.0.<br /><br />Klicken Sie auf <strong>weiter</strong>, um zu beginnen. - Code der Sprachkultur - Name der Sprachkultur + Code der Kultur + Name der Kultur Sie haben keine Tätigkeiten mehr durchgeführt und werden automatisch abgemeldet in @@ -523,18 +530,18 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Einen wunderbaren Sonntag - Frohen freundlichen Freitag - Donnerwetter Donnerstag Schönen Montag Einen großartigen Dienstag Wunderbaren Mittwoch + Donnerwetter Donnerstag + Frohen freundlichen Freitag Wunderbaren sonnigen Samstag Hier anmelden: - Die Sitzung ist abgelaufen - <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> + Sitzung abgelaufen + <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> - Armaturenbrett + Dashboard Bereiche Inhalt @@ -548,9 +555,9 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Es ist noch kein Element ausgewählt. Bitte wählen Sie ein Element aus der Liste aus, bevor Sie fortfahren. Das aktuelle Element kann aufgrund seines Dokumenttyps nicht an diese Stelle verschoben werden. Das ausgewählte Element kann nicht zu einem seiner eigenen Unterelemente verschoben werden. + Dieses Element kann nicht auf der obersten Ebene platziert werden. Diese Aktion ist nicht erlaubt, da Sie unzureichende Berechtigungen für mindestens ein untergeordnetes Element haben. Kopierte Elemente mit dem Original verknüpfen - Dieses Element kann nicht auf der obersten Ebene platziert werden. Bearbeiten Sie Ihre Benachrichtigungseinstellungen für '%0%' @@ -565,8 +572,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Ihr freundlicher Umbraco-Robot -Hi %0%

    - +Hallo %0%

    Dies ist eine automatisch E-Mail, welche Sie informiert, dass die Aufgabe '%1%' an der Seite '%2%' vom Benutzer '%3%' ausgeführt wurde. @@ -591,7 +597,8 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die

    Einen schönen Tag wünscht

    Ihr freundlicher Umbraco-Robot -

    ]]> +

    +]]>
    [%0%] Benachrichtigung: %1% ausgeführt an Seite '%2%' Benachrichtigungen @@ -619,8 +626,8 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Hinweise für die Durchführung des Updates Es ist ein Update für dieses Paket verfügbar. Sie können es direkt vom Umbraco-Paket-Repository herunterladen. Version des Pakets - Paket-Webseite aufrufen Versionsverlauf des Pakets + Paket-Webseite aufrufen Einfügen mit Formatierung (Nicht empfohlen) @@ -647,7 +654,10 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Wenn Sie einen einfachen Zugriffsschutz unter Verwendung eines einzelnen Logins mit Kennwort aktivieren wollen + %0% kann nicht veröffentlicht werden, da die Veröffentlichung zeitlich geplant ist. + %0% konnte nicht veröffentlicht werden, da einige Daten die Gültigkeitsprüfung nicht bestanden haben. %0% konnte nicht veröffentlicht werden, da ein Plug-In die Aktion abgebrochen hat. + %0% kann nicht veröffentlicht werden, da das übergeordnete Dokument nicht veröffentlicht ist. Unveröffentlichte Unterelemente einschließen Bitte warten, Veröffentlichung läuft... %0% Elemente veröffentlicht, %1% Elemente ausstehend ... @@ -655,22 +665,22 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die %0% und die untergeordneten Elemente wurden veröffentlicht %0% und alle untergeordneten Elemente veröffentlichen Mit <em>Ok</em> wird <strong>%0%</strong> veröffentlicht und auf der Website sichtbar.<br/><br />Sie können dieses Element mitsamt seinen untergeordneten Elementen veröffentlichen, indem Sie <em>Unveröffentlichte Unterelemente einschließen</em> aktivieren. - %0% konnte nicht veröffentlicht werden, da einige Daten die Gültigkeitsprüfung nicht bestanden haben. - %0% kann nicht veröffentlicht werden, da das übergeordnete Dokument nicht veröffentlicht ist. - %0% kann nicht veröffentlicht werden, da die Veröffentlichung zeitlich geplant ist. + + + Sie haben keine freigegeben Farben konfiguriert - Externer Link eingeben - Interne Seite auswählen - Link hinzufügen + Externen Link eingeben + Internen Link auswählen Beschriftung - In neuem Fenster öffnen - Neue Beschriftung eingeben - Link eingeben - - - Zurücksetzen - + Link + In neuem Fenster öffnen + Bezeichnung eingeben + Link eingeben + + + Zurücksetzen + Aktuelle Version Zeigt die Unterschiede zwischen der aktuellen und der ausgewählten Version an.<br />Text in <del>rot</del> fehlen in der ausgewählten Version, <ins>grün</ins> markierter Text wurde hinzugefügt. @@ -696,9 +706,15 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Statistiken Übersetzung Benutzer - Umbraco Forms - Hilfe - Analytics + Hilfe + Formulare + Auswertungen + + + go to + Hilfethemen zu + Video-Tutorials für + Die besten Umbraco-Video-Tutorials Standardvorlage @@ -708,24 +724,30 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Elementtyp Typ Stylesheet + Skript Stylesheet-Eigenschaft - Skript Registerkarte Registerkartenbeschriftung Registerkarten - Dieser Dokumenttyp verwendet Masterdokumenttyp aktiviert + Dieser Dokumenttyp verwendet als Masterdokumenttyp. Register vom Masterdokumenttyp werden nicht angezeigt und können nur im Masterdokumenttyp selbst bearbeitet werden Für dieses Register sind keine Eigenschaften definiert. Klicken Sie oben auf "neue Eigenschaft hinzufügen", um eine neue Eigenschaft hinzuzufügen. - Master Dokumententyp - Erstelle zugehörige Vorlage + Masterdokumenttyp + Zugehörige Vorlage anlegen + Sortierreihenfolge + Erstellungsdatum Sortierung abgeschlossen. Ziehen Sie die Elemente an ihre gewünschte neue Position. Bitte warten, die Seiten werden sortiert. Das kann einen Moment dauern. Bitte schließen Sie dieses Fenster nicht, bis der Sortiervorgang abgeschlossen ist. + Fehlgeschlagen + Unzureichende Benutzerberechtigungen. Vorgang kann nicht abgeschlossen werden. + Abgebrochen + Vorgang wurde durch eine benutzerdefinierte Erweiterung abgebrochen Das Veröffentlichen wurde von einem individuellen Ereignishandler abgebrochen Eigenschaft existiert bereits Eigenschaft erstellt @@ -742,17 +764,20 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Wörterbucheintrag gespeichert Veröffentlichung nicht möglich, da das übergeordnete Dokument nicht veröffentlicht ist. Inhalte veröffentlicht - und sichtbar auf der Webseite + Sichtbar auf der Webseite Inhalte gespeichert - Denken Sie daran die Inhalte zu veröffentlichen, um die Änderungen sichtbar zu machen + Denken Sie daran, die Inhalte zu veröffentlichen, um die Änderungen sichtbar zu machen Zur Abnahme eingereicht Die Änderungen wurden zur Abnahme eingereicht + Medium gespeichert + Medium fehlerfrei gespeichert Mitglied gespeichert Stylesheet-Regel gespeichert Stylesheet gespeichert Vorlage gespeichert Fehler beim Speichern des Benutzers. Benutzer gespeichert + Benutzertyp gepsichert Datei wurde nicht gespeichert Datei konnte nicht gespeichert werden. Bitte überprüfen Sie die Schreibrechte auf Dateiebene. Datei gespeichert @@ -771,14 +796,16 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die XSLT kann nicht gespeichert werden. Bitte überprüfen Sie die Schreibrechte auf Dateiebene. XSLT gespeichert Keine Fehler im XSLT - Medium gespeichert Veröffentlichung des Inhalts aufgehoben Partielle Ansicht gespeichert Partielle Ansicht ohne Fehler gespeichert. Partielle Ansicht nicht gespeichert Fehler beim Speichern der Datei. - Benutzertyp gepsichert - Medium fehlerfrei gespeichert + Skript gespeichert + Skript fehlerfrei gespeichert! + Skript nicht gespeichert + Fehler beim Speichern der Datei. + Fehler beim Speichern der Datei. Gewünschter CSS-Selektor, zum Beispiel 'h1', '.bigHeader' oder 'p.infoText' @@ -800,68 +827,41 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Vorlage - Typ auswählen - Wählen Sie ein Layout für diese Seite - und fügen Sie ihr erstes Element hinzu.]]> - - Klicken zum Einfügen - Klicken zum Bild Einfügen - Bildbeschriftung... - Schreibe hier... - - Grid Layouts - Layouts sind der gesamte Arbeitsbereich des Grid Editors. Sie brauchen aber meistens nur ein oder zwei unterschiedliche Layouts. - Grid Layout hinzufügen - Layout einstellen (Spaltenbreite festlegen und zusätzliche Bereiche hinzufügen) - - Zeilenkonfiguration - Zeilen sind vordefinierte und horizontal angeordnete Zellen - Zeilenkonfiguration hinzufügen - Zeilen einstellen (Zellenbreite festlegen und zusätzliche Zellen hinzufügen) - + Element hinzufügen + Zeilenlayout auswählen + Einfach auf <i class="icon icon-add blue"></i> klicken, um das erste Element anzulegen + Drop content + Klicken, um Inhalt einzubetten + Klicken, um Abbildung einzufügen + Beschriftung ... + Hier schreiben ... + Layouts + Layouts sind die grundlegenden Arbeitsflächen für das Gestaltungsraster. Üblicherweise sind nicht mehr als ein oder zwei Layouts nötig. + Layout hinzufügen + Passen Sie das Layout an, indem Sie die Spaltenbreiten einstellen und Abschnitte hinzufügen. + Einstellungen für das Zeilenlayout + Zeilen sind vordefinierte horizontale Zellenanordnungen + Zeilenlayout hinzufügen + Pasen Sie das Zeilenlayout an, indem Sie die Zellenbreite einstellen und Zellen hinzufügen. Spalten - Gesamtanzahl von allen kombinierten Spalten im Grid Layout - + Insgesamte Spaltenanzahl im Layout Einstellungen - Konfiguriere, welche Einstellungen der Editor ändern kann - - - Stilangaben - Konfiguriere, welche Stilangaben Editoren ändern können - - Einstellungen werden nur mit eingetragener und gültiger JSON Konfiguration gespeichert - - Allen Editoren erlauben - Alle Zeilenkonfigurationen erlauben + Legen Sie fest, welche Einstellungen die Autoren anpassen können. + CSS-Stile + Legen Sie fest, welche Stile die Autoren anpassen können. + Die Einstellungen werden nur gespeichert, wenn die angegebene JSON-Konfiguration gültig ist. + Alle Elemente erlauben + Alle Zeilenlayouts erlauben - - - - - - - - - - - - - - - - - - - - - - + Alternatives Feld Alternativer Text Groß- und Kleinschreibung + Kodierung Feld auswählen Zeilenumbrüche ersetzen Ersetzt Zeilenumbrüche durch das HTML-Tag <br /> + Benutzerdefinierte Felder nur Datum Als Datum formatieren HTML kodieren @@ -875,15 +875,13 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Rekursiv Textabsatz entfernen Alle <p> am Anfang und am Ende des Feldinhalts werden entfernt + Standardfelder Großbuchstaben URL kodieren Wandelt Sonderzeichen zur Verwendung in URLs um Wird nur verwendet, wenn beide vorgenannten Felder leer sind Dieses Feld wird nur verwendet, wenn das primäre Feld leer ist Datum und Zeit mit Trennzeichen: - Benutzerdefinierte Felder - Standardfelder - Kodierung Ihre Aufgaben @@ -928,7 +926,7 @@ Ihr freundlicher Umbraco-Robot Zwischenspeicher Papierkorb Erstellte Pakete - Datentypen + Datentypen Wörterbuch Installierte Pakete Design-Skin installieren @@ -938,10 +936,10 @@ Ihr freundlicher Umbraco-Robot Makros Medientypen Mitglieder - Mitgliedergruppen + Mitgliedergruppen Mitgliederrollen - Mitglieder-Typen - Dokumententypen + Mitglieder-Typen + Dokumententypen Pakete Pakete Python-Dateien @@ -953,7 +951,7 @@ Ihr freundlicher Umbraco-Robot Stylesheets Vorlagen XSLT-Dateien - Analysen + Auswertungen Neues Update verfügbar @@ -964,10 +962,10 @@ Ihr freundlicher Umbraco-Robot Administrator Feld für Kategorie - Passwort ändern - Neues Passwort - Neues Kennwort (Bestätigung) - Sie können Ihr Kennwort für den Zugriff auf den Umbraco-Verwaltungsbereich ändern, indem Sie das nachfolgende Formular ausfüllen und auf die 'Kennwort ändern'-Schaltfläche klicken + Kennwort ändern + Neues Kennwort + Neues Kennwort (Bestätigung) + Sie können Ihr Kennwort für den Zugriff auf den Umbraco-Verwaltungsbereich ändern, indem Sie das nachfolgende Formular ausfüllen und auf 'Kennwort ändern' klicken Schnittstelle für externe Editoren Feld für Beschreibung Benutzer endgültig deaktivieren @@ -983,12 +981,12 @@ Ihr freundlicher Umbraco-Robot Kennwort zurücksetzen Ihr Kennwort wurde geändert! Bitte bestätigen Sie das neue Kennwort - Aktuelle Kennwort - Ungültig aktuelle Kennwort Geben Sie Ihr neues Kennwort ein Ihr neues Kennwort darf nicht leer sein! + Aktuelles Kennwort + Aktuelles Kennwort falsch Ihr neues Kennwort und die Wiederholung Ihres neuen Kennworts stimmen nicht überein. Bitte versuchen Sie es erneut! - Die Wiederholung Ihres Kennworts stimmt nicht mit dem neuen Kennwort überein! + Die Bestätigung Ihres Kennworts stimmt nicht mit dem angegebenen neuen Kennwort überein! Die Berechtigungen der untergeordneten Elemente ersetzen Die Berechtigungen für folgende Seiten werden angepasst: Dokumente auswählen, um deren Berechtigungen zu ändern @@ -999,9 +997,9 @@ Ihr freundlicher Umbraco-Robot Rolle Rollen Autor - Übersetzer - Ihr Profil - Ihr Verlauf - Sitzung läuft ab in + Übersetzer + Ihr Profil + Ihr Verlauf + Sitzung läuft ab in
    diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 79fad32639..84dedbb364 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -25,7 +25,7 @@ Public access Publish Unpublish - Reload nodes + Reload Republish entire site Restore Permissions @@ -53,7 +53,7 @@ Domain '%0%' has been updated Edit Current Domains
    One-level paths in domains are supported, eg. "example.com/en". However, they + "https://www.example.com/". One-level paths in domains are supported, eg. "example.com/en". However, they should be avoided. Better use the culture setting above.]]>
    Inherit Culture @@ -65,6 +65,7 @@ Viewing for + Clear selection Select Select current folder Do something else @@ -89,11 +90,13 @@ Save Save and publish Save and send for approval + Save list view Preview Preview is disabled because there's no template assigned Choose style Show styles Insert table + Generate models To change the document type for the selected content, first select from the list of valid types for this location. @@ -133,6 +136,7 @@ This item has been changed after publication This item is not published Last published + There are no items to show There are no items to show in the list. Media Type Link to media item(s) @@ -144,6 +148,7 @@ Properties This document is published but is not visible because the parent '%0%' is unpublished Oops: this document is published but is not in the cache (internal error) + Oops: this document is published but its url would collide with content %0% Publish Publication Status Publish at @@ -168,6 +173,10 @@ Click to upload Drop your files here... + Link to media + or click here to choose files + Only allowed file types are + Max file size is Create a new member @@ -179,6 +188,7 @@ Choose a type and a title "document types".]]> "media types".]]> + Document Type without a template Browse your website @@ -189,6 +199,12 @@ Visit Welcome + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Name Manage hostnames @@ -236,6 +252,42 @@ Click on the image to see full size Pick item View Cache Item + Create folder... + + Relate to original + The friendliest community + + Link to page + + Opens the linked document in a new window or tab + Opens the linked document in the full body of the window + Opens the linked document in the parent frame + + Link to media + + Select media + Select icon + Select item + Select link + Select macro + Select content + Select member + Select member group + No icons were found + + There are no parameters for this macro + + External login providers + Exception Details + Stacktrace + Inner Exception + + Link your + Un-Link your + + account + + Select editor Enter your username Enter your password + Confirm your password Name the %0%... Enter a name... + Label... + Enter a description... Type to search... Type to filter... Type to add tags (press enter after each tag)... + Enter your email Allow at root @@ -297,10 +353,17 @@ %0% is not in a correct format + Received an error from the server The specified file type has been disallowed by the administrator NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. Please fill both alias and name on the new property type! There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Error loading userControl '%0%' + Error loading customControl (Assembly: %0%, Type: '%1%') + Error loading MacroEngine script (file: %0%) + "Error parsing XSLT file: %0% + "Error reading XSLT file: %0% Please enter a title Please choose a type You're about to make the picture larger than the original size. Are you sure that you want to proceed? @@ -321,7 +384,9 @@ Actions Add Alias + All Are you sure? + Back Border by Cancel @@ -358,6 +423,7 @@ Inner margin Insert Install + Invalid Justify Language Layout @@ -367,6 +433,7 @@ Log off Logout Macro + Mandatory Move More Name @@ -397,6 +464,7 @@ Show page on Send Size Sort + Submit Type Type to search... Up @@ -413,7 +481,46 @@ Yes Folder Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + + + Black + Green + Yellow + Orange + Blue + Red + + + + Add tab + Add property + Add editor + Add template + Add child node + Add child + + Edit data type + + Navigate sections + + Shortcuts + show shortcuts + + Toggle list view + Toggle allow as root + + Background colour Bold @@ -421,6 +528,7 @@ Font Text + Page @@ -436,7 +544,7 @@ Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

    To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

    - Click the retry button when + Click the retry button when done.
    More information on editing web.config here.

    ]]>
    @@ -447,9 +555,9 @@ Press the upgrade button to upgrade your database to Umbraco %0%

    Don't worry - no content will be deleted and everything will continue working afterwards! -

    +

    ]]>
    - Press Next to + Press Next to proceed. ]]> next to continue the configuration wizard]]> The Default users' password needs to be changed!]]> @@ -458,7 +566,7 @@ The password is changed! - Umbraco creates a default user with a login ('admin') and password ('default'). It's important that the password is + Umbraco creates a default user with a login ('admin') and password ('default'). It's important that the password is changed to something unique.

    @@ -493,7 +601,7 @@ ]]> I want to start from scratch learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. ]]> @@ -507,8 +615,8 @@ I want to start with a simple website - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, Runway offers an easy foundation based on best practices to get you started faster than ever. If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages.

    @@ -529,9 +637,9 @@ You installed Runway, so why not see how your new website looks.]]>
    Further help and information Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, + started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, you can find plenty of resources on our getting started pages.]]>
    Launch Umbraco To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> @@ -562,6 +670,17 @@ To manage your website, simply open the Umbraco back office and start adding con Log in below Session timed out © 2001 - %0%
    Umbraco.com

    ]]>
    + Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + Your username to login to the Umbraco back-office is: %0%

    Click here to reset your password or copy/paste this URL into your browser:

    %1%

    ]]> +
    Dashboard @@ -599,13 +718,13 @@ To manage your website, simply open the Umbraco back office and start adding con ]]>
    Hi %0%

    -

    This is an automated mail to inform you that the task '%1%' +

    This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'

    @@ -617,7 +736,7 @@ To manage your website, simply open the Umbraco back office and start adding con

    @@ -630,7 +749,7 @@ To manage your website, simply open the Umbraco back office and start adding con - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + button and locating the package. Umbraco packages usually have a ".zip" extension. ]]> Author Demonstration @@ -689,6 +808,9 @@ To manage your website, simply open the Umbraco back office and start adding con %0% could not be published because the item is scheduled for release. ]]>
    + @@ -698,14 +820,14 @@ To manage your website, simply open the Umbraco back office and start adding con - Include unpublished child pages + Include unpublished subpages Publishing in progress - please wait... %0% out of %1% pages have been published... %0% has been published %0% and subpages have been published Publish %0% and all its subpages - ok to publish %0% and thereby making its content publicly available.

    - You can publish this page and all it's sub-pages by checking publish all children below. + Publish to publish %0% and thereby making its content publicly available.

    + You can publish this page and all its subpages by checking Include unpublished subpages below. ]]>
    @@ -777,13 +899,18 @@ To manage your website, simply open the Umbraco back office and start adding con No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. Master Document Type Create matching template + Add icon + Sort order + Creation date Sorting complete. Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items
    Do not close this window during sorting]]>
    + Validation + Validation errors must be fixed before the item can be saved Failed Insufficient user permissions, could not complete the operation Cancelled @@ -823,6 +950,8 @@ To manage your website, simply open the Umbraco back office and start adding con File saved File saved without any errors Language saved + Media Type saved + Member Type saved Python script not saved Python script could not be saved due to error Python script saved @@ -866,22 +995,26 @@ To manage your website, simply open the Umbraco back office and start adding con Quick Guide to Umbraco template tags Template - - Insert control - Choose a layout for this section - below and add your first element]]> + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here Click to embed Click to insert image Image caption... Write here... - - Grid layouts + + Grid Layouts Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add grid layout + Add Grid Layout Adjust the layout by setting column widths and adding additional sections - Row configurations Rows are predefined cells arranged horizontally Add row configuration @@ -889,11 +1022,10 @@ To manage your website, simply open the Umbraco back office and start adding con Columns Total combined number of columns in the grid layout - + Settings Configure what settings editors can change - Styles Configure what styling editors can change @@ -901,6 +1033,72 @@ To manage your website, simply open the Umbraco back office and start adding con Allow all editors Allow all row configurations + Set as default + Choose extra + Choose default + are added + + + + + Compositions + You have not added any tabs + Add new tab + Add another tab + Inherited from + Add property + Required label + + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + + Allowed Templates + Choose which templates editors are allowed to use on content of this type + + Allow as root + Allow editors to create content of this type in the root of the content tree + Yes - allow content of this type in the root + + Allowed child node types + Allow content of the specified types to be created underneath content of this type + + Choose child node + + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + + Available editors + Reuse + Editor settings + + Configuration + + Yes, delete + + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + + All Document types + All Documents + All media items + + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + + and all documents using this type + and all media items using this type + and all members using this type + + using this editor will get updated with the new settings + + Member can edit + Show on member profile + @@ -935,7 +1133,7 @@ To manage your website, simply open the Umbraco back office and start adding con Tasks assigned to you - assigned to you. To see a detailed view including comments, click on "Details" or just the page name. + assigned to you. To see a detailed view including comments, click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link.
    To close a translation task, please go to the Details view and click the "Close" button. ]]>
    @@ -964,11 +1162,12 @@ To manage your website, simply open the Umbraco back office and start adding con [%0%] Translation task for %1% No translator users found. Please create a translator user before you start sending content to translation Tasks created by you - created by you. To see a detailed view including comments, - click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. + created by you. To see a detailed view including comments, + click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. To close a translation task, please go to the Details view and click the "Close" button. ]]> The page '%0%' has been send to translation + Please select the language that the content should be translated into Send the page '%0%' to translation Assigned by Task opened @@ -985,7 +1184,7 @@ To manage your website, simply open the Umbraco back office and start adding con Cache Browser Recycle Bin Created packages - Data Types + Data Types Dictionary Installed packages Install skin @@ -995,10 +1194,11 @@ To manage your website, simply open the Umbraco back office and start adding con Macros Media Types Members - Member Groups + Member Groups Roles - Member Types - Document Types + Member Types + Document Types + Relation Types Packages Packages Python Files @@ -1022,7 +1222,7 @@ To manage your website, simply open the Umbraco back office and start adding con Administrator Category field Change Your Password - Change Your Password + New password Confirm new password You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button Content Channel @@ -1036,6 +1236,7 @@ To manage your website, simply open the Umbraco back office and start adding con Start Node in Media Library Sections Disable Umbraco Access + Old password Password Reset password Your password has been changed! @@ -1057,8 +1258,17 @@ To manage your website, simply open the Umbraco back office and start adding con User types Writer Translator + Change Your profile Your recent history Session expires in + + Validation + Validate as email + Validate as a number + Validate as a Url + ...or enter a custom validation + Field is mandatory +
    diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 4da1607c4b..ec41299b25 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -25,7 +25,7 @@ Public access Publish Unpublish - Reload nodes + Reload Republish entire site Restore Permissions @@ -53,7 +53,7 @@ Domain '%0%' has been updated Edit Current Domains
    One-level paths in domains are supported, eg. "example.com/en". However, they + "https://www.example.com/". One-level paths in domains are supported, eg. "example.com/en". However, they should be avoided. Better use the culture setting above.]]>
    Inherit Culture @@ -65,9 +65,10 @@ Viewing for + Clear selection Select Select current folder - Do something else + Do something else Bold Cancel Paragraph Indent Insert form field @@ -89,11 +90,14 @@ Save Save and publish Save and send for approval + Save list view Preview Preview is disabled because there's no template assigned Choose style Show styles Insert table + Generate models + Save and generate models To change the document type for the selected content, first select from the list of valid types for this location. @@ -133,6 +137,7 @@ This item has been changed after publication This item is not published Last published + There are no items to show There are no items to show in the list. Media Type Link to media item(s) @@ -144,6 +149,7 @@ Properties This document is published but is not visible because the parent '%0%' is unpublished Oops: this document is published but is not in the cache (internal error) + Oops: this document is published but its url would collide with content %0% Publish Publication Status Publish at @@ -161,13 +167,18 @@ Remove file(s) Link to document Member of group(s) - Not a member of group(s) + Not a member of group(s) Child items Target Click to upload Drop your files here... + Link to media + or click here to choose files + Only allowed file types are + Cannot upload this file, it does not have an approved file type + Max file size is Create a new member @@ -179,6 +190,7 @@ Choose a type and a title "document types".]]> "media types".]]> + Document Type without a template Browse your website @@ -189,6 +201,12 @@ Visit Welcome + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Name Manage hostnames @@ -236,6 +254,41 @@ Click on the image to see full size Pick item View Cache Item + Create folder... + + Relate to original + The friendliest community + + Link to page + + Opens the linked document in a new window or tab + Opens the linked document in the full body of the window + Opens the linked document in the parent frame + + Link to media + + Select media + Select icon + Select item + Select link + Select macro + Select content + Select member + Select member group + + There are no parameters for this macro + + External login providers + Exception Details + Stacktrace + Inner Exception + + Link your + Un-Link your + + account + + Select editor Enter your username Enter your password + Confirm your password Name the %0%... Enter a name... + Label... + Enter a description... Type to search... Type to filter... Type to add tags (press enter after each tag)... + Enter your email @@ -298,10 +355,17 @@ %0% is not in a correct format + Received an error from the server The specified file type has been disallowed by the administrator NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. Please fill both alias and name on the new property type! There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Error loading userControl '%0%' + Error loading customControl (Assembly: %0%, Type: '%1%') + Error loading MacroEngine script (file: %0%) + "Error parsing XSLT file: %0% + "Error reading XSLT file: %0% Please enter a title Please choose a type You're about to make the picture larger than the original size. Are you sure that you want to proceed? @@ -322,7 +386,9 @@ Actions Add Alias + All Are you sure? + Back Border by Cancel @@ -359,6 +425,7 @@ Inner margin Insert Install + Invalid Justify Language Layout @@ -368,6 +435,7 @@ Log off Logout Macro + Mandatory Move More Name @@ -386,6 +454,7 @@ Properties Email to receive form data Recycle Bin + Your recycle bin is empty Remaining Rename Renew @@ -393,11 +462,13 @@ Retry Permissions Search + Sorry, we can not find what you are looking for Server Show Show page on Send Size Sort + Submit Type Type to search... Up @@ -414,6 +485,42 @@ Yes Folder Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + + + Black + Green + Yellow + Orange + Blue + Red + + + Add tab + Add property + Add editor + Add template + Add child node + Add child + + Edit data type + + Navigate sections + + Shortcuts + show shortcuts + + Toggle list view + Toggle allow as root Background color @@ -437,7 +544,7 @@ Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

    To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

    - Click the retry button when + Click the retry button when done.
    More information on editing web.config here.

    ]]>
    @@ -448,23 +555,19 @@ Press the upgrade button to upgrade your database to Umbraco %0%

    Don't worry - no content will be deleted and everything will continue working afterwards! -

    +

    ]]>
    - Press Next to - proceed. ]]> + + Press Next to + proceed. ]]> + next to continue the configuration wizard]]> The Default users' password needs to be changed!]]> The Default user has been disabled or has no access to Umbraco!

    No further actions needs to be taken. Click Next to proceed.]]> The Default user's password has been successfully changed since the installation!

    No further actions needs to be taken. Click Next to proceed.]]> The password is changed! - - Umbraco creates a default user with a login ('admin') and password ('default'). It's important that the password is - changed to something unique. -

    -

    - This step will check the default user's password and suggest if it needs to be changed. -

    + + ('admin') and password ('default'). It's important that the password is changed to something unique. ]]> Get a great start, watch our introduction videos By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. @@ -493,8 +596,9 @@ It also stores temporary data (aka: cache) for enhancing the performance of your website. ]]>
    I want to start from scratch - + learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. ]]> @@ -506,10 +610,11 @@ ]]>
    Only recommended for experienced users I want to start with a simple website - + - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, Runway offers an easy foundation based on best practices to get you started faster than ever. If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages.

    @@ -530,9 +635,9 @@ You installed Runway, so why not see how your new website looks.]]>
    Further help and information Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, + started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, you can find plenty of resources on our getting started pages.]]>
    Launch Umbraco To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> @@ -562,7 +667,18 @@ To manage your website, simply open the Umbraco back office and start adding con Happy Caturday Log in below Session timed out - © 2001 - %0%
    Umbraco.com

    ]]>
    + © 2001 - %0%
    Umbraco.com

    ]]>
    + Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + Your username to login to the Umbraco back-office is: %0%

    Click here to reset your password or copy/paste this URL into your browser:

    %1%

    ]]> +
    Dashboard @@ -598,15 +714,16 @@ To manage your website, simply open the Umbraco back office and start adding con Cheers from the Umbraco robot ]]>
    - Hi %0%

    + + Hi %0%

    -

    This is an automated mail to inform you that the task '%1%' +

    This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%'

    @@ -618,7 +735,7 @@ To manage your website, simply open the Umbraco back office and start adding con

    @@ -690,6 +807,9 @@ To manage your website, simply open the Umbraco back office and start adding con %0% could not be published because the item is scheduled for release. ]]>
    + @@ -699,14 +819,14 @@ To manage your website, simply open the Umbraco back office and start adding con - Include unpublished child pages + Include unpublished subpages Publishing in progress - please wait... %0% out of %1% pages have been published... %0% has been published %0% and subpages have been published Publish %0% and all its subpages - ok to publish %0% and thereby making its content publicly available.

    - You can publish this page and all it's sub-pages by checking publish all children below. + Publish to publish %0% and thereby making its content publicly available.

    + You can publish this page and all its subpages by checking Include unpublished subpages below. ]]>
    @@ -778,13 +898,18 @@ To manage your website, simply open the Umbraco back office and start adding con No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. Master Document Type Create matching template + Add icon + Sort order + Creation date Sorting complete. Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items
    Do not close this window during sorting]]>
    + Validation + Validation errors must be fixed before the item can be saved Failed Insufficient user permissions, could not complete the operation Cancelled @@ -824,6 +949,8 @@ To manage your website, simply open the Umbraco back office and start adding con File saved File saved without any errors Language saved + Media Type saved + Member Type saved Python script not saved Python script could not be saved due to error Python script saved @@ -868,19 +995,25 @@ To manage your website, simply open the Umbraco back office and start adding con Template - Insert control - Choose a layout for this section - below and add your first element]]> + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here Click to embed Click to insert image Image caption... Write here... - Grid layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add grid layout - Adjust the layout by setting column widths and adding additional sections + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections Row configurations Rows are predefined cells arranged horizontally Add row configuration @@ -900,7 +1033,79 @@ To manage your website, simply open the Umbraco back office and start adding con Allow all editors Allow all row configurations + Set as default + Choose extra + Choose default + are added + + + Compositions + You have not added any tabs + Add new tab + Add another tab + Inherited from + Add property + Required label + + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree + Yes - allow content of this type in the root + + Allowed child node types + Allow content of the specified types to be created underneath content of this type + + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + + Available editors + Reuse + Editor settings + + Configuration + + Yes, delete + + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + + All Document types + All Documents + All media items + + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + + and all documents using this type + and all media items using this type + and all members using this type + + using this editor will get updated with the new settings + + Member can edit + Show on member profile + tab has no sort order + + + + Building models + this can take abit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + Alternative field Alternative Text @@ -933,7 +1138,7 @@ To manage your website, simply open the Umbraco back office and start adding con Tasks assigned to you - assigned to you. To see a detailed view including comments, click on "Details" or just the page name. + assigned to you. To see a detailed view including comments, click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link.
    To close a translation task, please go to the Details view and click the "Close" button. ]]>
    @@ -962,11 +1167,12 @@ To manage your website, simply open the Umbraco back office and start adding con [%0%] Translation task for %1% No translator users found. Please create a translator user before you start sending content to translation Tasks created by you - created by you. To see a detailed view including comments, - click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. + created by you. To see a detailed view including comments, + click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link. To close a translation task, please go to the Details view and click the "Close" button. ]]> The page '%0%' has been send to translation + Please select the language that the content should be translated into Send the page '%0%' to translation Assigned by Task opened @@ -983,7 +1189,7 @@ To manage your website, simply open the Umbraco back office and start adding con Cache Browser Recycle Bin Created packages - Data Types + Data Types Dictionary Installed packages Install skin @@ -993,10 +1199,11 @@ To manage your website, simply open the Umbraco back office and start adding con Macros Media Types Members - Member Groups - Roles - Member Types - Document Types + Member Groups + Member Roles + Member Types + Document Types + Relation Types Packages Packages Python Files @@ -1020,7 +1227,7 @@ To manage your website, simply open the Umbraco back office and start adding con Administrator Category field Change Your Password - Change Your Password + New password Confirm new password You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button Content Channel @@ -1034,6 +1241,7 @@ To manage your website, simply open the Umbraco back office and start adding con Start Node in Media Library Sections Disable Umbraco Access + Old password Password Reset password Your password has been changed! @@ -1055,8 +1263,17 @@ To manage your website, simply open the Umbraco back office and start adding con User types Writer Translator + Change Your profile Your recent history Session expires in + + Validation + Validate as email + Validate as a number + Validate as a Url + ...or enter a custom validation + Field is mandatory + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml index fab7d30f03..80e39fe219 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml @@ -185,6 +185,12 @@ Visita Bienvenido + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Nombre Administrar dominios @@ -393,6 +399,7 @@ Mostrar página al enviar Tamaño Ordenar + Submit Tipo Tipo que buscar... Arriba @@ -407,6 +414,8 @@ Bienvenido... Ancho Si + Reorder + I am done reordering Color de fondo @@ -644,6 +653,8 @@ Crear template correspondiente + Sort order + Creation date Ordenación completa. Arrastra las diferentes páginas debajo para colocarlas como deberían estar. O haz click en las cabeceras de las columnas para ordenar todas las páginas @@ -717,14 +728,20 @@ Insertar control + Choose layout Añade más filas - y añade tu primer contenido]]> + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here Plantillas de Grid Las plantillas son el área de trabajo para el editor de grids, normalmente sólo necesitas una o dos plantillas diferentes Añadir plantilla de grid Ajusta la plantilla configurando la anchura de las columnas y añadiendo más secciones - + Configuraciones de filas Las filas son celdas predefinidas que se disponen horizontalmente Añade una configuración de fila @@ -732,7 +749,7 @@ Columnas Número total de columnas en la plantilla del grid - + Configuración Configura qué ajustes pueden cambiar los editores @@ -814,7 +831,7 @@ Caché del navegador Papelera de reciclaje Paquetes creados - Tipos de datos + Tipos de datos Diccionario Paquetes instalados Instalar skin @@ -824,10 +841,10 @@ Macros Tipos de medios Miembros - Grupos de miembros + Grupos de miembros Roles - Tipos de miembros - Tipos de documento + Tipos de miembros + Tipos de documento Paquetes Paquetes Ficheros Python diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index af1c39345b..59021956a5 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -181,6 +181,12 @@ Visiter Bienvenue + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Name Gérer les noms d'hôtes @@ -380,6 +386,7 @@ Afficher la page à l'envoi Taille Trier + Submit Type Rechercher... Haut @@ -396,6 +403,8 @@ Oui Dossier Résultats de recherche + Reorder + I am done reordering Background color @@ -755,6 +764,8 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Créer le template correspondant + Sort order + Creation date Tri achevé. Faites glisser les différents éléments ci-dessous vers le haut ou le bas pour définir comment ils doivent être triés. Ou cliquez sur les entêtes de colonne pour trier la collection complète d'éléments
    Ne fermez pas cette fenêtre durant le tri.]]>
    @@ -833,6 +844,45 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Guide rapide aux tags des modèles Umbraco Modèle + + Choisissez le type de contenu + Choisissez une mise en page + Ajouter une ligne + Ajouter du contenu + Contenu goutte + Paramètres appliqués + + Ce contenu est pas autorisée ici + Ce contenu est permis ici + + Cliquez pour intégrer + Cliquez pour insérer l'image + Légende de l'image... + Ecrire ici... + + Layouts Grid + Layouts sont la superficie totale de travail pour l'éditeur de grille, en général, vous avez seulement besoin d'une ou deux configurations différentes + Ajouter Grid Layout + Ajustez la mise en page en définissant la largeur des colonnes et ajouter des sections supplémentaires + Configurations des lignes + Les lignes sont des cellules prédéfinies disposées horizontalement + Ajouter une configuration de la ligne + Ajustez la ligne en réglant la largeur des cellules et en ajoutant des cellules supplémentaires + + Colonnes + Nombre total combiné de colonnes dans la configuration de la grille + + Paramètres + Configurer quels paramètres éditeurs peuvent changer + + Modes + Configurer ce style éditeurs peuvent changer + + Les réglages seulement économiser si la configuration du json saisi est valide + + Autoriser tous les éditeurs + Autoriser toutes les configurations de lignes + Champ alternatif Texte alternatif @@ -915,7 +965,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Navigateur de cache Corbeille Packages créés - Typesde données + Typesde données Dictionnaire Packages installés Installer un skin @@ -925,10 +975,10 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à Macros Types de médias Membres - Groupes de membres + Groupes de membres Rôles - Types de membres - Types de documents + Types de membres + Types de documents Packages Packages Fichiers Python diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml index 81cf4b2ff5..aa9c2112e2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml @@ -131,6 +131,12 @@ בקר ברוכים הבאים + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + שם ניהול שם מתחם @@ -316,6 +322,7 @@ הצג עמוד בשליחה גודל סדר + Submit סוג הקלד לחיפוש... למעלה @@ -330,6 +337,8 @@ ברוכים הבאים... רוחב כן + Reorder + I am done reordering צבע רקע @@ -652,6 +661,8 @@ To manage your website, simply open the Umbraco back office and start adding con לשוניות + Sort order + Creation date המיון הושלם. יש לגרור את הפריטים מעלה או מטה כדי להגדיר את סדר התוכן. או לחץ על כותרת העמודה כדי למיין את כל פריטי התוכן
    נא לא לסגור את החלון בזמן המיון]]>
    @@ -722,6 +733,45 @@ To manage your website, simply open the Umbraco back office and start adding con מדריך מהיר עבור תבנית תגיות באומברקו תבנית + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + שדה אלטרנטיבי טקסט חלופי @@ -800,7 +850,7 @@ To manage your website, simply open the Umbraco back office and start adding con זיכרון מטמון בדפדפן סל מיחזור יצירת חבילות - סוגי מידע + סוגי מידע מילון חבילות מותקנות התקן עיצוב @@ -810,10 +860,10 @@ To manage your website, simply open the Umbraco back office and start adding con מקרו סוגי מדיה משתמשים - קבוצות משתמשים + קבוצות משתמשים כללים - סוגי משתמשים - סוגי מסמכים + סוגי משתמשים + סוגי מסמכים חבילות חבילות קבצי פייתון diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml index 975ce0bb6a..9c0682eba0 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml @@ -127,6 +127,12 @@ Visita Benvenuto + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Nome Gestione alias Hostnames @@ -309,6 +315,7 @@ Mostra la pagina inviata Dimensione Ordina + Submit Tipo Su @@ -323,6 +330,8 @@ Benvenuto... Larghezza Si + Reorder + I am done reordering Colore di sfondo @@ -616,6 +625,8 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Tabs + Sort order + Creation date
    Non chiudere questa finestra durante l'operazione]]>
    @@ -692,6 +703,45 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Template + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + @@ -773,7 +823,7 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Cache Browser Cestino Pacchetti creati - Tipi di dato + Tipi di dato Dizionario Pacchetti installati Installare skin @@ -783,10 +833,10 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Macros Tipi di media Membri - Gruppi di Membri + Gruppi di Membri Ruoli - Tipologia Membri - Tipi di documento + Tipologia Membri + Tipi di documento Pacchetti Pacchetti Files Python diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml index db3fb72574..6acfd997e1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml @@ -52,10 +52,10 @@ ドメイン '%0%' は既に割り当てられています ドメイン '%0%' は更新されました ドメインの編集 - - Inherit + + 継承 カルチャの割り当て - + ドメイン割り当て @@ -67,7 +67,7 @@ その他のアクション 太字 インデント解除 - フィールドから挿入 + フォーム フィールドの挿入 グラフィックヘッドラインの挿入 HTMLの編集 インデント @@ -86,11 +86,13 @@ 保存 保存及び公開 保存して承認に送る + リスト ビューの保存 プレビュー テンプレートが指定されていないのでプレビューは無効になっています スタイルの選択 スタイルの表示 表の挿入 + モデルを生成 ドキュメントタイプを変更するには、まず有効なドキュメントタイプのリストから選択します @@ -104,10 +106,10 @@ 割り当てるプロパティ 新しいテンプレート 新しいドキュメントタイプ - None + なし コンテント ドキュメントタイプを変更する - プロパティが以下のように割り当てられました + 選択コンテンツのドキュメント タイプは、[新しいタイプ] に変更され、以下のプロパティがマップされました。 から 1つ以上のプロパティを割り当てられませんでした。プロパティが定義が重複しています 有効なドキュメントタイプのみが表示されます @@ -125,11 +127,12 @@ 作成日時 このドキュメントが作成された日時 ドキュメントタイプ - 変種中 + 編集中 公開終了日時 このページは公開後変更されています このページは公開されていません 公開日時 + 表示するアイテムはありません リストに表示するアイテムはありません メディアタイプ メディアの項目へのリンク @@ -165,6 +168,10 @@ クリックしてアップロードする ファイルをここへドロップ.. + メディアへリンク + またはクリックしてファイルを選択 + 使用可能なファイル タイプ + ファイルの最大サイズ メンバーの新規作成 @@ -176,6 +183,7 @@ 型とタイトルを選んでください "document types".]]> "media types".]]> + テンプレートなしのドキュメント タイプ ウェブサイトを参照する @@ -186,6 +194,12 @@ 訪れる ようこそ + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + 名前 ドメインの割り当て @@ -224,8 +238,8 @@ サイトは再インデックスされました ウェブサイトのキャッシュがリフレッシュされました。 公開されているコンテンツはリフレッシュされましたが、非公開のコンテンツは非公開のままです。 ウェブサイトのキャッシュがリフレッシュされます。 公開されているコンテンツはリフレッシュされますが、非公開のコンテンツは非公開のままです。 - 行数 - 列数 + 列数 + 行数 プレースホルダにidを設定して、子テンプレートからもこのテンプレートをコンテントに入れられるようにできます。 IDは<asp:content />エレメントとして用います。]]> プレースホルダのidを選択してください。 @@ -233,6 +247,41 @@ クリックすると画像がフルサイズで表示されます 項目の選択 キャッシュされている項目の表示 + フォルダーの作成... + + オリジナルに関連付ける + フレンドリーなコミュニティ + + ページへリンク + + リンク ドキュメントを新しいウィンドウまたはタブで開く + リンク ドキュメントをウィンドウ全文表示で開く + 親フレームでリンク ドキュメントを開く + + メディアへリンク + + メディアの選択 + アイコンの選択 + アイテムの選択 + リンクの選択 + マクロの選択 + コンテンツの選択 + メンバーの選択 + メンバー グループの選択 + + このマクロのパラメーターはありません + + 外部ログイン プロバイダー + 例外の詳細 + スタックトレース + Inner Exception + + 次をリンク: + 次をリンク解除: + + アカウント + + エディターの選択 パスワードを入力... %0%と命名します... ここに名称を入力してください... + ラベル... + 説明を入力してください... 検索する... 条件で絞り込む... - タグを追加します... + 入力してタグを追加 (各タグの後に Enter を押してください)... ルートノードとして許可する これを有効にするとコンテンツとメディアツリーのルートレベルに作成することができます 子ノードとして許可するタイプ - Document Type Compositions + ドキュメント タイプ構成 新規作成 - 削除 + タブの削除 説明 - 新規見出し - 見出し + 新しいタブ + タブ サムネイル リストビューを有効にする 子ノードをツリーに表示せずにリストビューに表示します @@ -270,8 +321,8 @@ 値の前に追加 データベースのデータ型 - データ型のGUID - 対応させるコントロール + プロパティ エディター GUID + プロパティ エディター ボタン 高度な設定を有効にする コンテキストメニューを有効にする @@ -294,10 +345,17 @@ %0% は正しい書式ではありません + サーバー エラーが発生しました 指定されたファイルタイプは管理者のみに許可されます 注意! CodeMirrorが設定で有効化されていますが、 Internet Explorerでは不安定なので無効化してください。 新しいプロパティ型のエイリアスと名前の両方を設定してください! 特定のファイルまたはフォルタの読み込み/書き込みアクセスに問題があります + Partial View スクリプトの読み込みエラー (ファイル: %0%) + userControl の読み込みエラー '%0%' + customControl の読み込みエラー (アセンブリ: %0%, タイプ: '%1%') + MacroEngine スクリプトの読み込みエラー (ファイル: %0%) + XSLT ファイル解析エラー: %0% + XSLT ファイル読み込みエラー: %0% タイトルを入力してください 型を選択してください 元画像より大きくしようとしていますが、本当によろしいのですか? @@ -318,6 +376,7 @@ アクション選択 追加 エイリアス + すべて 確かですか? 枠線 または @@ -355,6 +414,7 @@ 内側の余白 挿入 インストール + 無効 位置揃え 言語 レイアウト @@ -394,6 +454,7 @@ 送信後にページを表示 サイズ 並べ替え + 送信 検索... @@ -410,7 +471,46 @@ はい フォルダー 検索結果 + 順序変更 + 順序変更終了 + プレビュー + パスワードの変更 + ->  + リスト ビュー + 保存... + 現在 + 移動 + 埋め込み + + + ブラック + グリーン + イエロー + オレンジ + ブルー + レッド + + + + タブの追加 + プロパティの追加 + エディターの追加 + テンプレートの追加 + 子ノードの追加 + 子の追加 + + データ タイプの編集 + + セクションの移動 + + ショートカット + ショートカットの表示 + + リスト ビューの切り替え + ルートとして許可に切り替え + + 背景色 太字 @@ -444,7 +544,7 @@ アップグレードボタンを押すとUmbraco %0% 用にデータベースをアップグレードします。

    心配ありません。 - コンテントが消える事はありませんし、後で続けることもできます。 -

    +

    ]]>
    次へ を押して続行してください。]]> @@ -490,7 +590,7 @@ ]]>
    スクラッチから始めたい どうしたらいいの?) 後からRunwayをインストールする事もできます。そうしたくなった時は、Developerセクションのパッケージへどうぞ。 ]]> @@ -504,8 +604,8 @@ 簡単なウェブサイトから始めたい - "Runway"(≈滑走路)は幾つかの基本的なテンプレートから簡単なウェブサイトを用意します。このインストーラーは自動的にRnwayをセットアップできますが、 - これを編集したり、拡張したり、削除する事も簡単にできます。もしUmbracoを完璧に使いこなせるならばこれは不要です。とはいえ、 + "Runway"(≈滑走路)は幾つかの基本的なテンプレートから簡単なウェブサイトを用意します。このインストーラーは自動的にRnwayをセットアップできますが、 + これを編集したり、拡張したり、削除する事も簡単にできます。もしUmbracoを完璧に使いこなせるならばこれは不要です。とはいえ、 Runwayを使う事は、手間なく簡単にUmbracoを始める為には良い選択肢です。 Runwayをインストールすれば、必要に応じてRunwayによる基本的な構成のページをRunwayのモジュールから選択できます。

    @@ -549,17 +649,16 @@ Runwayをインストールして作られた新しいウェブサイトがど 作業を保存して今すぐ更新 - Happy super sunday - Happy manic monday - Happy tubular tuesday - Happy wonderful wednesday - Happy thunder thursday - Happy funky friday - Happy caturday + ハッピー スーパー日曜日 + ハッピー マニアック月曜日 + ハッピー最高の火曜日 + ハッピー ワンダフル水曜日 + ハッピー サンダー木曜日 + ハッピー ファンキー金曜日 + ハッピー土曜日 ウェブサイトにログインします。 セッションタイムアウトしました。 © 2001 - %0%
    umbraco.org

    ]]>
    - Umbraco にようこそ。ユーザー名とパスワードを入力してください: ダッシュボード @@ -576,6 +675,7 @@ Runwayをインストールして作られた新しいウェブサイトがど まだノードが選択されていません。'ok'をクリックする前に上のリストでノードを選択してください。 現在のノードは、ドキュメントタイプの設定により選択されたノードの子になることはできません。 ノードは、自分のサブページには移動できません + 現在のノードはルートにできません。 子ドキュメントで権限がないので、その操作はできません。 コピーしたものを元と関係づける @@ -599,7 +699,7 @@ Runwayをインストールして作られた新しいウェブサイトがど

    ユーザー '%3%' によりページ '%2%' 上のタスク '%1%' から自動的にメールします。

    @@ -611,7 +711,7 @@ Runwayをインストールして作られた新しいウェブサイトがど

    @@ -627,8 +727,8 @@ Runwayをインストールして作られた新しいウェブサイトがど パッケージを選択できます。Umbracoのパッケージは概ね".zip"ないしは".umb"といった拡張子です。 ]]>
    作成者 - 文書 - 文書 + デモ + ヘルプ パッケージのメタデータ パッケージ名 パッケージには何も含まれません @@ -681,6 +781,9 @@ Runwayをインストールして作られた新しいウェブサイトがど + 設定済みの色はありません。 - 外部リンクを追加 - 内部リンクを追加 - 追加 - タイトル - キャプション表示を入力 - 内部リンクを選択 外部リンクを入力 - リンクを入力 - 内部ページ - リンク - URL - 下に移動 - 上に移動 - 新規ウィンドウで開く - リンクを削除 + 内部リンクを選択 + キャプション + リンク + 新規ウィンドウで開く + キャプションを入力 + リンクを入力 リセット @@ -780,13 +875,18 @@ Runwayをインストールして作られた新しいウェブサイトがど このタブにはプロパティが定義されていません、上部のリンクから新しいプロパティを作成してください マスタードキュメントタイプ テンプレートを作成する + アイコンの追加 + Sort order + Creation date ソートが完了しました。 上下にアイテムをドラッグするなどして適当に配置したり、列のヘッダーをクリックしてコレクション全体をソートできます
    並び替え中はウィンドウを閉じないでください。]]>
    + 検証 + アイテムを保存する前に検証エラーを修正してください。 失敗しました 不十分なユーザー権限により操作を完了できませんでした キャンセルされました @@ -844,6 +944,11 @@ Runwayをインストールして作られた新しいウェブサイトがど 部分ビューをエラーなしで保存しました! 部分ビューは保存されていません ファイルを保存するときにエラーが発生しました。 + スクリプト ビューが保存されました + スクリプト ビューが正しく保存されました + スクリプト ビューが保存されていません + ファイルの保存でエラーが発生しました。 + ファイルの保存でエラーが発生しました。 CSSシンタックスを使用 例: h1, .redHeader, .blueTex @@ -857,7 +962,7 @@ Runwayをインストールして作られた新しいウェブサイトがど テンプレートの編集 コンテンツ領域の挿入 コンテンツ領域プレースホルダーの挿入 - dictionary item の挿入 + ディクショナリ アイテムを挿入 マクロの挿入 umbraco ページフィールドの挿入 マスターテンプレート @@ -867,8 +972,14 @@ Runwayをインストールして作られた新しいウェブサイトがど 挿入するアイテムを選択する - ここからレイアウトを選択します - 最初の要素を追加します]]> + レイアウトを選択 + 行の追加 + コンテンツの追加 + コンテンツのドロップ + 適用される設定 + + このコンテンツはここでは許可されていません + このコンテンツはここに使用できます クリックして埋め込む クリックして画像を挿入する @@ -899,6 +1010,66 @@ Runwayをインストールして作られた新しいウェブサイトがど すべてのエディタを許可する すべての行の構成を許可する + デフォルトとして設定 + 追加を選択 + デフォルトの選択 + 追加されました + + + + + 構成 + タブが追加されていません + 新しいタブの追加 + 他のタブの追加 + 次から継承: + プロパティの追加 + 必要なラベル + + リスト ビューの有効化 + 並べ替えと検索が可能な子のリストを表示するコンテンツ アイテムを設定します。子はツリーに表示されません。 + + 利用可能なテンプレート + このタイプのコンテンツで使用できるテンプレート エディターを選択してください + + ルートとして許可 + コンテンツ ツリーのルートでこのタイプのコンテンツをエディターで作成することを許可 + はい - ルートでこのタイプのコンテンツを許可 + + 許可された子ノード タイプ + このタイプの下部コンテンツに指定タイプのコンテンツを作成することを許可 + + 子ノードの選択 + + 既存ドキュメント タイプのタブとプロパティを継承。新しいタブを現在のドキュメント タイプに追加、または同じ名前のタブがある場合はマージされます。 + このコンテンツ タイプが構成で使用されるため、自身を構成することはできません。 + 構成に使用できるコンテンツ タイプはありません。 + + 使用可能なエディター + 再利用 + エディター設定 + + 設定 + + 削除します + + 下部に移動しました。 + 以下のツリー構造へ移動する + フォルダーを選択します + + すべてのドキュメント タイプ + すべてのドキュメント + すべてのメディア アイテム + + このドキュメント タイプを使用すると完全に削除されます。削除してもよろしいですか? + このメディア タイプを使用すると完全に削除されます。削除してもよろしいですか? + このメンバー タイプを使用すると完全に削除されます。削除してもよろしいですか? + + およびこのタイプを使用したすべてのドキュメント + およびこのタイプを使用したこのメディア アイテム + およびこのタイプを使用したすべてのメンバー + + このエディターを使用すると新しい設定で更新されます @@ -917,7 +1088,7 @@ Runwayをインストールして作られた新しいウェブサイトがど フィールドの値の後ろに追加される フィールドの値の前に追加される 小文字変換 - None + なし フィールド値の後ろ追加 フィールド値の前に追加 再帰的 @@ -981,7 +1152,7 @@ Runwayをインストールして作られた新しいウェブサイトがど キャッシュの参照 ごみ箱 パッケージの作成 - データ型 + データ型 ディクショナリ インストール済のパッケージ スキンのインストール @@ -991,10 +1162,11 @@ Runwayをインストールして作られた新しいウェブサイトがど マクロ メディアタイプ メンバー - メンバーのグループ + メンバーのグループ 役割 - メンバーの種類 - ドキュメントタイプ + メンバーの種類 + ドキュメントタイプ + 関連タイプ パッケージ パッケージ Python ファイル @@ -1053,8 +1225,17 @@ Runwayをインストールして作られた新しいウェブサイトがど ユーザーの種類 投稿者 翻訳者 + 変更 あなたのプロフィール あなたの最新の履歴 セッションの期限 + + 検証 + メールで検証 + 数値で検証 + URL で検証 + ... またはカスタム検証を入力 + 必須フィールドです + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml index b3f6a3a384..c7178e312b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml @@ -125,6 +125,12 @@ 방문 환영합니다 + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + 이름 호스트네임 관리 @@ -309,6 +315,7 @@ 전송된 페이지보기 사이즈 정렬 + Submit 타입 검색유형... 위로 @@ -323,6 +330,8 @@ 환영합니다... 너비 + Reorder + I am done reordering 배경색 @@ -628,6 +637,8 @@ 색인 + Sort order + Creation date 정렬 완료 다른 아이템을 마우스로 위,아래로 드래그 하여 이동하거나 열의 헤더를 클릭하여 아이템을 정렬할 수 있습니다
    정렬하는 동안 이 창을 닫지 마십시오]]>
    @@ -698,6 +709,45 @@ Umbraco 템플릿태그 퀵가이드 템플릿 + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + 대체 필드 대체 글꼴 @@ -777,7 +827,7 @@ 캐시 브라우저 휴지통 생성된 패키지 - 데이터 타입 + 데이터 타입 사전 설치된 패키지 TRANSLATE ME: 'Install skin' @@ -787,10 +837,10 @@ 매크로 미디어 타입 구성원 - 구성원 그룹 + 구성원 그룹 역할 - 구성원 유형 - 문서 타입 + 구성원 유형 + 문서 타입 패키지 패키지 Python 파일 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index 0320df479e..7d1a4cc2d3 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -188,6 +188,12 @@ Bezoek Welkom + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Naam Beheer domeinnamen @@ -397,6 +403,7 @@ Toon pagina bij versturen Formaat Sorteren + Submit Type Type om te zoeken... Omhoog @@ -412,6 +419,8 @@ Breedte Ja Map + Reorder + I am done reordering Zoekresultaten @@ -727,6 +736,8 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Maak een bijbehorend template + Sort order + Creation date Sorteren gereed. Sleep de pagina's omhoog of omlaag om de volgorde te veranderen. Of klik op de kolom-header om alle pagina's daarop te sorteren.
    Sluit dit venster niet tijdens het sorteren]]>
    @@ -807,8 +818,14 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Item toevoegen + Choose a layout Een rij aan de lay-out toevoegen teken onderaan en voeg je eerste item toe]]> + Drop content + Settings applied + + This content is not allowed here + This content is allowed here Klik om een item te embedden Klik om een afbeelding in te voegen @@ -915,7 +932,7 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Cachebrowser Prullenbak Gemaakte packages - Datatypes + Datatypes Woordenboek Geïnstalleerde packages Installeer skin @@ -925,10 +942,10 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Macro's Mediatypes Leden - Ledengroepen + Ledengroepen Rollen - Ledentypes - Documenttypes + Ledentypes + Documenttypes Packages Packages Python-bestanden @@ -985,6 +1002,7 @@ Om een vertalingstaak te sluiten, ga aub naar het detailoverzicht en klik op de Gebruikerstype Gebruikerstypes Auteur + Wijzig Je profiel Je recente historie diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml index 1b59676ae7..c2c8b4d073 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml @@ -125,6 +125,12 @@ Odwiedź Witaj + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Nazwa Zarządzaj nazwami hostów @@ -307,6 +313,7 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]>
    Pokaż stronę "wyślij" Rozmiar Sortuj + Submit Typ Szukaj W górę @@ -321,6 +328,8 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]>
    Witaj... Szerokość Tak + Reorder + I am done reordering Kolor tła @@ -544,6 +553,8 @@ Miłego dnia!]]>
    Zakładki + Sort order + Creation date Sortowanie zakończone. Przesuń poszczególne elementy w górę oraz w dół aż będą w odpowiedniej kolejności, lub kliknij na nagłówku kolumny aby posortować całą kolekcję elementów
    Nie zamykaj tego okna podczas sortowania]]>
    @@ -614,6 +625,45 @@ Miłego dnia!]]>
    Szybki przewodnik po tagach szablonu Umbraco Szablon + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + Pole alternatywne Tekst alternatywny @@ -673,7 +723,7 @@ Miłego dnia!]]> Cache przeglądarki Kosz Utworzone pakiety - Typy danych + Typy danych Słownik Zainstalowane pakiety TRANSLATE ME: 'Install skin' @@ -683,10 +733,10 @@ Miłego dnia!]]> Makra Typy mediów Członkowie - Grupy członków + Grupy członków Role - Typ członka - Typy dokumentów + Typ członka + Typy dokumentów Pakiety Pakiety Pliki Python diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml index a713ba8d5b..016f7c2a19 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml @@ -125,6 +125,12 @@ Visitar Bem Vindo(a) + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Nome Gerenciar hostnames @@ -306,6 +312,7 @@ Mostrar página durante envio Tamanho Classificar + Submit Tipo Digite para buscar... Acima @@ -320,6 +327,8 @@ Bem Vindo(a)... Largura Sim + Reorder + I am done reordering Cor de fundo @@ -617,6 +626,8 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Guias + Sort order + Creation date Classificação concluída. Arraste os diferentes itens para cima ou para baixo para definir como os mesmos serão arranjados. Ou clique no título da coluna para classificar a coleção completa de itens
    Não feche esta janela durante a classificação]]>
    @@ -687,6 +698,45 @@ Você pode publicar esta página e todas suas sub-páginas ao selecionar pub Guia rápido para etiquetas de modelos Umbraco Modelo + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + Campo alternativo Texto alternativo @@ -761,7 +811,7 @@ Para fechar a tarefa de tradução vá até os detalhes e clique no botão "Fech Navegador de Cache Lixeira Pacotes criados - Tipo de Dado + Tipo de Dado Dicionário Pacotes instalados Instalar tema @@ -771,10 +821,10 @@ Para fechar a tarefa de tradução vá até os detalhes e clique no botão "Fech Macros Tipos de Mídia Membros - Grupos de Membros + Grupos de Membros Funções - Tipo de Membro - Tipos de Documentos + Tipo de Membro + Tipos de Documentos Pacotes Pacotes Arquivos Python diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index d5bfbaffb1..e162dbaead 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -68,6 +68,7 @@ Полужирный Уменьшить отступ Вставить поле формы + Сгенерировать модели Вставить графический заголовок Править исходный код HTML Увеличить отступ @@ -84,8 +85,10 @@ Править связи Вернуться к списку Сохранить + Сохранить и построить модели Сохранить и опубликовать Сохранить и направить на публикацию + Сохранить список Выбрать Выбрать текущую папку Предварительный просмотр @@ -119,6 +122,14 @@ Вы не указали ни одного допустимого цвета + + Черный + Зеленый + Желтый + Оранжевый + Синий + Красный + Об этой странице Алиас @@ -138,6 +149,7 @@ Этот документ был изменен после публикации Этот документ не опубликован Документ опубликован + Здесь еще нет элементов. В этом списке пока нет элементов. Ссылка на медиа-элементы Тип медиа-контента @@ -169,12 +181,69 @@ Удалить файл Ссылка на документ + + Композиции + Вы не добавили ни одной вкладки + Добавить вкладку + Добавитьеще вкладку + Унаследован от + Добавить свойство + Обязательная метка + + Представление в формате списка + Устанавливает представление документа данного типа в виде сортируемого списка дочерних документов с функцией поиска, в отличие от обычного представления дочерних документов в виде дерева + + Допустимые шаблоны + Выберите перечень допустимых шаблонов для сопоставления документам данного типа + Разрешить в качестве корневого + Позволяет создавать документы этого типа в самом корне дерева содержимого + ДА - разрешить создание в качестве корневого + + Допустимые типы дочерних документов + Позволяет указать перечень типов документов, допустимых для создания документов, дочерних к данному типу + + Выбрать дочерний узел + Унаследовать вкладки и свойства из уже существующего типа документов. Вкладки будут либо добавлены в создаваемый тип, либо в случае совпадения названий вкладок будут добавлены наследуемые свойства. + Этот тип документов уже участвует в композиции другого типа, поэтому сам не может быть композицией. + В настоящее время нет типов документов, допустимых для построения композиции. + + Доступные редакторы + Переиспользовать + Установки редактора + + Конфигурирование + + ДА, удалить + + была перемещена внутрь + Выбрать папку для перемещения + в структуре дерева + + Все типы документов + Все документы + Все медиа-элементы + + , использующие этот тип документов, будут безвозвратно удалены, пожалуйста, подтвердите это действие. + , использующие этот тип медиа, будут безвозвратно удалены, пожалуйста, подтвердите это действие. + , использующие этот тип участников, будут безвозвратно удалены, пожалуйста, подтвердите это действие. + + и все документы, использующие данный тип + и все медиа-элементы, использующие данный тип + и все участники, использующие данный тип + + , использующие этот редактор, будут обновлены с применением этих установок + + Участник может изменить + Показать в профиле участника + для вкладки не указан порядок сортировки + Где вы хотите создать новый %0% Создать в узле "Типы документов".]]> "Типы медиа-материалов".]]> Выберите тип и заголовок + Типу документа не сопоставлен шаблон Обзор сайта @@ -185,6 +254,12 @@ Посетить Рады приветствовать + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Название Управление доменами @@ -232,10 +307,45 @@ Кликните на изображении, чтобы увидеть полноразмерную версию Выберите элемент Просмотр элемента кэша + Создать папку... + + Связать с оригиналом + Самое дружелюбное сообщество + + Ссылка на страницу + + Открывает документ по ссылке в новом окне или вкладке браузера + Открывает документ по ссылке в полноэкранном режиме + Открывает документ по ссылке в родительском фрейме + + Ссылка на медиа-файл + + Выбрать медиа + Выбрать значок + Выбрать элемент + Выбрать ссылку + Выбрать макрос + Выбрать содержимое + Выбрать участника + Выбрать группу участников + + Это макрос без параметров + + Провайдеры аутентификации + Подробное сообщение об ошибке + Трассировка стека + Inner Exception + + Связать + Разорвать связь + + учетную запись + + Выбрать редактор %0%'
    Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева + Ниже Вы можете указать различные переводы данной статьи словаря '%0%'
    Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева ]]> Название языка (культуры) @@ -284,10 +394,17 @@ %0% - данные в некорректном формате + Получено сообщение об ошибке от сервера ПРЕДУПРЕЖДЕНИЕ! Несмотря на то, что CodeMirror по-умолчанию разрешен в данной конфигурации, он по-прежнему отключен для браузеров Internet Explorer ввиду нестабильной работы Укажите, пожалуйста, алиас и имя для этого свойства! Использование данного типа файлов на сайте запрещено администратором Ошибка доступа к указанному файлу или папке + Ошибка загрузки кода в частичном представлении (файл: %0%) + Ошибка загрузки пользовательского элемента управления '%0%' + Ошибка загрузки внешнего типа (сборка: %0%, тип: '%1%') + Ошибка загрузки макроса (файл: %0%) + "Ошибка разбора кода XSLT в файле: %0% + "Ошибка чтения XSLT-файла: %0% Ошибка в конфигурации типа данных, используемого для свойства, проверьте тип данных Укажите заголовок Выберите тип @@ -308,7 +425,9 @@ Действия Добавить Алиас + Все Вы уверены? + Назад Границы пользователем Отмена @@ -346,6 +465,7 @@ Внутренний отступ Вставить Установить + Неверно Выравнивание Язык Макет @@ -355,6 +475,7 @@ Выйти Выход Макрос + Обязательно Больше Переместить Название @@ -373,6 +494,7 @@ Свойства Email адрес для получения данных Корзина + Ваша корзина пуста Осталось Переименовать Обновить @@ -380,12 +502,14 @@ Повторить Разрешения Поиск + К сожалению, ничего подходящего не нашлось Результаты поиска Сервер Показать Показать страницу при отправке Размер Сортировать + Отправить Тип Что искать? Вверх @@ -400,6 +524,16 @@ Добро пожаловать... Ширина Да + Пересортировать + Пересортировка завершена + Предпросмотр + Сменить пароль + к + Список + Сохранение... + текущий + Переместить + Внедрить Цвет фона @@ -409,7 +543,8 @@ Текст - снизу и добавьте Ваш первый элемент]]> + Добавить содержимое + Сбросить содержимое Добавить шаблон сетки Настройте шаблон, задавая ширину колонок или добавляя дополнительные секции Добавить конфигурацию строки @@ -417,9 +552,12 @@ Добавить новые строки Доступны все редакторы Доступны все конфигурации строк + Выберите шаблон Кликните для встраивания Кликните для вставки изображения Колонки + Недопустимый тип содержимого + Данный тип содержимого разрешен Суммарное число колонок в шаблоне сетки Шаблоны сетки Шаблоны являются рабочим пространством для редактора сетки, обычно Вам понадобится не более одного или двух шаблонов @@ -430,9 +568,14 @@ Строки - это последовательности ячеек с горизонтальным расположением Установки будут сохранены только если они указаны в корректном формате json Установки + Установки применены Задайте установки, доступные редакторам для изменения Стили Задайте стили, доступные редакторам для изменения + Установить по-умолчанию + Выбрать дополнительно + Выбрать по-умолчанию + добавлены Страница @@ -471,22 +614,18 @@

    Пожалуйста, не волнуйтесь, ни одной строки Вашей базы данных не будет потеряно при данной операции, и после ее завершения все будет работать!

    ]]> - - Нажмите кнопку Далее для продолжения.]]> + + Нажмите Далее для продолжения. ]]> + Далее для продолжения работы мастера настроек]]> Пароль пользователя по-умолчанию необходимо сменить!]]> Пользователь по-умолчанию заблокирован или не имеет доступа к Umbraco!

    Не будет предпринято никаких дальнейших действий. Нажмите кнопку Далее для продолжения.]]> Пароль пользователя по-умолчанию успешно изменен в процессе установки!

    Нет надобности в каких-либо дальнейших действиях. Нажмите кнопку Далее для продолжения.]]> Пароль изменен! Umbraco создает пользователя по-умолчанию - с именем входа ('admin') - и паролем ('default'). - ОЧЕНЬ ВАЖНО - изменить пароль по-умолчанию на что-либо уникальное.

    -

    Данный шаг произведет проверку пароля для пользователя по-умолчанию - и предложит сменить этот пароль в случае необходимости.

    - ]]>
    +

    Umbraco создает пользователя по-умолчанию с именем входа ('admin') и паролем ('default'). + ОЧЕНЬ ВАЖНО изменить пароль по-умолчанию на что-либо уникальное.

    ]]> + Для начального обзора возможностей системы рекомендуем посмотреть ознакомительные видеоматериалы Далее (или модифицируя вручную ключ "UmbracoConfigurationStatus" в файле "web.config"), Вы принимаете лицензионное соглашение для данного программного обеспечения, расположенное ниже. Пожалуйста, обратите внимание, что инсталляционный пакет Umbraco отвечает двум различным типам лицензий: лицензии MIT на программные продукты с открытым исходным кодом в части ядра системы и свободной лицензии Umbraco в части пользовательского интерфейса.]]> Система не установлена. @@ -593,11 +732,22 @@ Нажмите, чтобы загрузить Перетащите файлы сюда... + Ссылка на файл + или нажмите сюда, чтобы выбрать файлы + Разрешены только типы файлов: + Максимально допустимый размер файла: Создать нового участника Все участники + + Построение моделей + это может занять некоторое время, пожалуйста, подождите + Модели построены + Модели не могут быть построены + Процесс построения моделей завершился ошибкой, подробности в системном журнале Umbraco + Выберите страницу... Узел %0% был скопирован в %1% @@ -703,6 +853,8 @@ Пароль Что искать... Имя пользователя + Метка... + Укажите описание... Расширенный: Защита на основе ролей (групп) @@ -729,6 +881,9 @@ + @@ -782,6 +937,7 @@ Статистика Перевод Пользователи + Добавить значок в качестве родительского типа. Вкладки родительского типа не показаны и могут быть изменены непосредственно в родительском типе @@ -803,7 +959,27 @@ Заголовок вкладки Вкладки + + Добавить вкладку + Добавить свойство + Добавить редактор + Добавить шаблон + Добавить дочерний узел + Добавить дочерний + + Изменить тип данных + + Навигация по разделам + + Ярлыки + показать ярлыки + + В формате списка + Разрешить в качестве корневого + + Порядок сортировки + Дата создания Сортировка завершена Перетаскивайте элементы на нужное место вверх или вниз для определения необходимого Вам порядка сортировки. Также можно щелкнуть по заголовкам столбцов, чтобы отсортировать все элементы сразу.
    Не закрывайте это окно до окончания процесса сортировки.]]>
    @@ -845,6 +1021,8 @@ Файл сохранен Файл сохранен без ошибок Язык сохранен + Тип медиа сохранен + Тип участника сохранен Представление не сохранено Произошла ошибка при сохранении файла Представление сохранено @@ -937,7 +1115,7 @@ http://%3%. Удачи! - + Генератор уведомлений Umbraco. ]]> [%0%] Задание по переводу %1% @@ -965,7 +1143,7 @@ Обзор кэша Корзина Созданные пакеты - Типы данных + Типы данных Словарь Установленные пакеты Установить тему @@ -975,10 +1153,10 @@ Макросы Типы медиа-материалов Участники - Группы участников + Группы участников Роли участников - Типы участников - Типы документов + Типы участников + Типы документов Пакеты дополнений Пакеты дополнений Скрипты Python @@ -1015,6 +1193,7 @@ Разделы Новый пароль Отключить доступ к административной панели Umbraco + Прежний пароль Пароль Ваш пароль доступа изменен! Подвердите новый пароль @@ -1040,4 +1219,12 @@ Ваша недавняя история Ваш профиль + + Валидация + Валидация по формату email + Валидация числового значения + Валидация по формату Url + ...или указать свои правила валидации + Обязательно к заполению + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index 84b885a931..ea1f0c9ef0 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -181,6 +181,12 @@ Välkommen Besök + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Namn Hantera domännamn @@ -378,6 +384,7 @@ Vilken sida skall visas när formuläret är skickat Storlek Sortera + Submit Skriv Skriv för att söka... Upp @@ -392,6 +399,8 @@ Bredd Titta på Ja + Reorder + I am done reordering Bakgrundsfärg @@ -649,6 +658,8 @@ Skapa matchande mall + Sort order + Creation date Sortering klar Välj i vilken ordning du vill ha sidorna genom att dra dem upp eller ner i listan. Du kan också klicka på kolumnrubrikerna för att sortera grupper av sidor
    Stäng inte fönstret under tiden sidorna sorteras.]]>
    @@ -729,8 +740,13 @@ Lägg till + Choose layout Lägg till rad - nedan för att lägga till ditt första element]]> + Add content + Drop content + Styles applied + Indholdet er ikke tilladt her + Indholdet er tilladt her Klicka för att lägga in Klicka för att lägga till bild Bildtext... @@ -816,7 +832,7 @@ Cacha webbläsare Papperskorg Skapade paket - Datatyper + Datatyper Ordbok Installerade paket Installera skin @@ -826,10 +842,10 @@ Makron Mediatyper Medlem - Medlemsgrupper + Medlemsgrupper Roller - Medlemstyper - Dokumenttyper + Medlemstyper + Dokumenttyper Paket Paket Python-filer diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml index a45a626366..372bc480cc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml @@ -166,6 +166,12 @@ 访问 欢迎 + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + 锚点名称 管理主机名 @@ -349,6 +355,7 @@ 在发送时预览 大小 排序 + Submit 类型 输入内容开始查找… @@ -363,6 +370,8 @@ 欢迎… + Reorder + I am done reordering 背景色 @@ -695,6 +704,8 @@ 没有字段设置在该标签页 + Sort order + Creation date 排序完成。 上下拖拽项目或单击列头进行排序
    请不要关闭窗口]]>
    @@ -773,6 +784,45 @@ 模板标签快速指南 模板 + + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + + This content is not allowed here + This content is allowed here + + Click to embed + Click to insert image + Image caption... + Write here... + + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + + Columns + Total combined number of columns in the grid layout + + Settings + Configure what settings editors can change + + Styles + Configure what styling editors can change + + Settings will only save if the entered json configuration is valid + + Allow all editors + Allow all row configurations + 替代字段 替代文本 @@ -851,7 +901,7 @@ 缓存浏览 回收站 创建扩展包 - 数据类型 + 数据类型 字典 已安装的扩展包 安装皮肤 @@ -861,10 +911,10 @@ 媒体类型 会员 - 会员组 + 会员组 角色 - 会员类型 - 文档类型 + 会员类型 + 文档类型 扩展包 扩展包 Python文件 diff --git a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx deleted file mode 100644 index 21230a343d..0000000000 --- a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx +++ /dev/null @@ -1,240 +0,0 @@ -<%@ Control Language="c#" AutoEventWireup="True" Codebehind="ContentTypeControlNew.ascx.cs" - Inherits="Umbraco.Web.UI.Umbraco.Controls.ContentTypeControlNew" TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %> -<%@ Import Namespace="umbraco" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> -<%@ Register TagPrefix="cc2" Namespace="umbraco.uicontrols" Assembly="controls" %> -<%@ Register TagPrefix="cdf" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> - - - - - - - - - - -

    <%=ui.GetText("settings", "contentTypeEnabled")%>
    <%=umbraco.ui.GetText("settings", "contentTypeUses")%> <%=umbraco.ui.GetText("settings", "asAContentMasterType")%>

    -
    - - - -   - - - - - - - - - - - - - - - - - -

    -

    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    -
    - - - - - <%if (cb_isContainer.Checked) { %> - -
    - -
    -
    -
    -
    - - -
    - - -  (<%=ui.Text("general", "default", Security.CurrentUser) %>) - -
    - - <%=ui.Text("general", "edit", Security.CurrentUser) %> - -
    - -
    - - -
    - -
    -
    -
    - -
    -
    - - <%--Scripting to for configuring a list view--%> - - - <%} %> - -
    - -
    - - - - - - - - - - - - - - -
    - - - - -

    Master Content Type enabled
    This Content Type uses as a Master Content Type. Properties from Master Content Types are not shown and can only be edited on the Master Content Type itself

    -
    - - -
    - - -
    -
    -
    -<%-- cannot put a <%= block here 'cos it prevents the Controls collection from being modified = use a literal --%> - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx.cs b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx.cs deleted file mode 100644 index fef17e6e2e..0000000000 --- a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Web; -using System.Web.UI.WebControls; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web.Editors; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.UI.Umbraco.Controls -{ - public partial class ContentTypeControlNew : global::umbraco.controls.ContentTypeControlNew - { - protected string DataTypeControllerUrl { get; private set; } - protected string ContentTypeControllerUrl { get; private set; } - - /// - /// Raises the event. - /// - /// The object that contains the event data. - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - DataTypeControllerUrl = Url.GetUmbracoApiServiceBaseUrl(x => x.GetById(0)); - ContentTypeControllerUrl = Url.GetUmbracoApiServiceBaseUrl(x => x.GetAssignedListViewDataType(0)); - } - - protected void dgTabs_PreRender(object sender, EventArgs e) - { - dgTabs.UseAccessibleHeader = true; //to make sure we render th, not td - - Table table = dgTabs.Controls[0] as Table; - if (table != null && table.Rows.Count > 0) - { - // here we render and - if (dgTabs.ShowHeader) - table.Rows[0].TableSection = TableRowSection.TableHeader; - if (dgTabs.ShowFooter) - table.Rows[table.Rows.Count - 1].TableSection = TableRowSection.TableFooter; - } - } - - protected void dgTabs_ItemDataBound(object sender, DataGridItemEventArgs e) - { - Table table = dgTabs.Controls[0] as Table; - if (table != null && table.Rows.Count > 0) - { - if (dgTabs.ShowHeader) - table.Rows[0].TableSection = TableRowSection.TableHeader; - if (dgTabs.ShowFooter) - table.Rows[table.Rows.Count - 1].TableSection = TableRowSection.TableFooter; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx.designer.cs b/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx.designer.cs deleted file mode 100644 index 2ad0b89587..0000000000 --- a/src/Umbraco.Web.UI/umbraco/controls/ContentTypeControlNew.ascx.designer.cs +++ /dev/null @@ -1,24 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Umbraco.Web.UI.Umbraco.Controls { - - - public partial class ContentTypeControlNew { - - /// - /// JsInclude control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude; - } -} diff --git a/src/Umbraco.Web.UI/umbraco/controls/GenericProperties/GenericProperty.ascx b/src/Umbraco.Web.UI/umbraco/controls/GenericProperties/GenericProperty.ascx deleted file mode 100644 index 9ec9bbaca7..0000000000 --- a/src/Umbraco.Web.UI/umbraco/controls/GenericProperties/GenericProperty.ascx +++ /dev/null @@ -1,80 +0,0 @@ -<%@ Control Language="c#" AutoEventWireup="True" Inherits="umbraco.controls.GenericProperties.GenericProperty" TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %> -<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> - -
  • -
    -
    -
    - - - - - - - - - - - - -
    -
    - - -
    -
  • - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/create/content.ascx b/src/Umbraco.Web.UI/umbraco/create/content.ascx deleted file mode 100644 index 47ea26143d..0000000000 --- a/src/Umbraco.Web.UI/umbraco/create/content.ascx +++ /dev/null @@ -1,27 +0,0 @@ -<%@ Control Language="c#" AutoEventWireup="True" Codebehind="content.ascx.cs" Inherits="umbraco.cms.presentation.create.controls.content" TargetSchema="http://schemas.microsoft.com/intellisense/ie5" %> - - -
    <%=umbraco.ui.Text("name")%>:
    -

    -* - -<%=umbraco.ui.Text("choose")%> <%=umbraco.ui.Text("documentType")%>:
    - -* - -
    - -
    private void InitializeEditorForTemplate() { - - //TODO: implement content placeholders, etc... just like we had in v5 - //Panel1.Menu.InsertSplitter(); + //TODO: implement content placeholders, etc... just like we had in v5 - //MenuIconI umbContainer = Panel1.Menu.NewIcon(); - //umbContainer.ImageURL = UmbracoPath + "/images/editor/masterpagePlaceHolder.gif"; - //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); - //umbContainer.OnClickCommand = - // ClientTools.Scripts.OpenModalWindow( - // IOHelper.ResolveUrl(SystemDirectories.Umbraco) + - // "/dialogs/insertMasterpagePlaceholder.aspx?&id=" + _template.Id, - // ui.Text("template", "insertContentAreaPlaceHolder"), 470, 320); + editorSource.Menu.InsertSplitter(); - //MenuIconI umbContent = Panel1.Menu.NewIcon(); - //umbContent.ImageURL = UmbracoPath + "/images/editor/masterpageContent.gif"; - //umbContent.AltText = ui.Text("template", "insertContentArea"); - //umbContent.OnClickCommand = - // ClientTools.Scripts.OpenModalWindow( - // IOHelper.ResolveUrl(SystemDirectories.Umbraco) + "/dialogs/insertMasterpageContent.aspx?id=" + - // _template.Id, ui.Text("template", "insertContentArea"), 470, 300); - - } + MenuIconI umbRenderBody = editorSource.Menu.NewIcon(); + umbRenderBody.ImageURL = UmbracoPath + "/images/editor/renderbody.gif"; + //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); + umbRenderBody.AltText = "Insert @RenderBody()"; - } + umbRenderBody.OnClickCommand = "editViewEditor.insertRenderBody()"; + + MenuIconI umbSection = editorSource.Menu.NewIcon(); + umbSection.ImageURL = UmbracoPath + "/images/editor/masterpagePlaceHolder.gif"; + //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); + umbSection.AltText = "Insert Section"; + + umbSection.OnClickCommand = "editViewEditor.openSnippetModal('section')"; + + MenuIconI umbRenderSection = editorSource.Menu.NewIcon(); + umbRenderSection.ImageURL = UmbracoPath + "/images/editor/masterpageContent.gif"; + //umbContainer.AltText = ui.Text("template", "insertContentAreaPlaceHolder"); + umbRenderSection.AltText = "Insert @RenderSection"; + + umbRenderSection.OnClickCommand = "editViewEditor.openSnippetModal('rendersection')"; + + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/webService.asmx b/src/Umbraco.Web.UI/umbraco/webService.asmx deleted file mode 100644 index 19848579d6..0000000000 --- a/src/Umbraco.Web.UI/umbraco/webService.asmx +++ /dev/null @@ -1 +0,0 @@ -<%@ WebService Language="c#" Codebehind="webService.asmx.cs" Class="umbraco.webService" %> diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js b/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js index 66c97c4db7..63870fad08 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js +++ b/src/Umbraco.Web.UI/umbraco_client/Application/Extensions.js @@ -357,4 +357,20 @@ }); + //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 + $.ajaxSetup({ + beforeSend: function (xhr) { + + function getCookie(name) { + var value = "; " + document.cookie; + var parts = value.split("; " + name + "="); + if (parts.length === 2) return parts.pop().split(";").shift(); + } + + xhr.setRequestHeader("X-XSRF-TOKEN", getCookie("XSRF-TOKEN")); + } + }); + })(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js index 82cec69d81..88f385eac3 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js +++ b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoClientManager.js @@ -151,7 +151,6 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); /** This is used to launch an angular based modal window instead of the legacy window */ openAngularModalWindow: function (options) { - if (!this.mainWindow().UmbClientMgr) { throw "An angular modal window can only be launched when the modal is running within the main Umbraco application"; } @@ -173,6 +172,12 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); }, + reloadLocation: function () { + if (this.mainWindow().UmbClientMgr) { + this.mainWindow().UmbClientMgr.reloadLocation(); + } + }, + openModalWindow: function(url, name, showHeader, width, height, top, leftOffset, closeTriggers, onCloseCallback) { //need to create the modal on the top window if the top window has a client manager, if not, create it on the current window diff --git a/src/Umbraco.Web.UI/umbraco_client/CodeArea/UmbracoEditor.js b/src/Umbraco.Web.UI/umbraco_client/CodeArea/UmbracoEditor.js index 8f0faf2a28..96ef28f53c 100644 --- a/src/Umbraco.Web.UI/umbraco_client/CodeArea/UmbracoEditor.js +++ b/src/Umbraco.Web.UI/umbraco_client/CodeArea/UmbracoEditor.js @@ -35,9 +35,18 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls.CodeEditor"); this._control.val(code); } else { - //this is a wrapper for CodeMirror + //this is a wrapper for CodeMirror this._editor.focus(); + + var codeChanged = this._editor.getValue() != code; + var cursor = this._editor.doc.getCursor(); + this._editor.setValue(code); + + // Restore cursor position if the server saved code matches. + if (!codeChanged) + this._editor.setCursor(cursor); + this._editor.focus(); } }, diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js index cfd33541cb..b49e72fcc7 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js @@ -63,9 +63,17 @@ return ret; }, self._opts.invalidDomain); + function getDuplicateMessage(val, el) { + var other = $(el).nextAll('input').val(); + var msg = self._opts.duplicateDomain + if (other != "" && other != "!!!") + msg = msg + ' (' + other + ')'; + return msg; + } + $.validator.addMethod("duplicate", function (value, element, param) { - return $(element).nextAll('input').val() == 0 && !self._isRepeated($(element)); - }, self._opts.duplicateDomain); + return $(element).nextAll('input').val() == "" && !self._isRepeated($(element)); + }, getDuplicateMessage); $.validator.addClassRules({ domain: { domain: true }, @@ -80,7 +88,7 @@ $('form input.domain').live('focus', function(event) { if (event.type != 'focusin') return; - $(this).nextAll('input').val(0); + $(this).nextAll('input').val(""); }); // force validation *now* @@ -105,14 +113,14 @@ } else { var inputs = $('form input.domain'); - inputs.each(function() { $(this).nextAll('input').val(0); }); + inputs.each(function() { $(this).nextAll('input').val(""); }); for (var i = 0; i < json.Domains.length; i++) { var d = json.Domains[i]; - if (d.Duplicate == 1) + if (d.Duplicate) inputs.each(function() { var input = $(this); if (input.val() == d.Name) - input.nextAll('input').val(1); + input.nextAll('input').val(d.Other ? d.Other : "!!!"); }); } $('form').valid(); diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js index eb62afd805..dc74ad95ff 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js @@ -22,11 +22,11 @@ s = s.replace(/\-/g, "/"); //all of these basically transform the string into year-month-day since that //is what JS understands when creating a Date object - if (c.dateFormat.indexOf("dd/MM/yyyy") == 0 || c.dateFormat.indexOf("dd-MM-yyyy") == 0) { - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3-$2-$1"); + if (c.dateFormat.indexOf("dd/MM/yyyy") == 0 || c.dateFormat.indexOf("dd-MM-yyyy") == 0 || c.dateFormat.indexOf("dd.MM.yyyy") == 0) { + s = s.replace(/(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{4})/, "$3-$2-$1"); } - else if (c.dateFormat.indexOf("dd/MM/yy") == 0 || c.dateFormat.indexOf("dd-MM-yy") == 0) { - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$3-$2-$1"); + else if (c.dateFormat.indexOf("dd/MM/yy") == 0 || c.dateFormat.indexOf("dd-MM-yy") == 0 || c.dateFormat.indexOf("dd.MM.yy") == 0) { + s = s.replace(/(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2})/, "$3-$2-$1"); } else if (c.dateFormat.indexOf("MM/dd/yyyy") == 0 || c.dateFormat.indexOf("MM-dd-yyyy") == 0) { s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3-$1-$2"); @@ -118,4 +118,4 @@ -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditContentType.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditContentType.js deleted file mode 100644 index 18f47196b8..0000000000 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditContentType.js +++ /dev/null @@ -1,108 +0,0 @@ -Umbraco.Sys.registerNamespace("Umbraco.Editors"); - -(function ($) { - - var _model = {}; - var _opts = null; - - //updates the UI elements - function updateElements() { - _opts.configPanel.find("strong").html(_model.listViewName); - if (_model.isSystem) { - _opts.createListViewButton.show(); - _opts.removeListViewButton.hide(); - _opts.configPanel.find("em").show(); - } - else { - _opts.createListViewButton.hide(); - _opts.removeListViewButton.show(); - _opts.configPanel.find("em").hide(); - } - } - - function populateData() { - //get init data - - $.get(_opts.contentTypeServiceBaseUrl + "GetAssignedListViewDataType?contentTypeId=" + _opts.contentTypeId, function (result) { - _model.isSystem = result.isSystem; - _model.listViewName = result.name; - _model.listViewId = result.id; - updateElements(); - }); - } - - $.ajaxSetup({ - beforeSend: function (xhr) { - xhr.setRequestHeader("X-XSRF-TOKEN", $.cookie("XSRF-TOKEN")); - }, - contentType: 'application/json;charset=utf-8', - dataType: "json", - dataFilter: function (data, dataType) { - if ((typeof data) === "string") { - //trim the csrf bits off - data = data.replace(/^\)\]\}\'\,\n/, ""); - } - return data; - } - }); - - Umbraco.Editors.EditContentType = base2.Base.extend({ - - // Constructor - constructor: function(opts) { - // Merge options with default - _opts = $.extend({ - // Default options go here - }, opts); - }, - - init: function () { - //wire up handlers - - _opts.configPanel.find("a").click(function() { - UmbClientMgr.contentFrame('#/developer/datatype/edit/' + _model.listViewId); - }); - - _opts.isContainerChk.on("change", function () { - if ($(this).is(":checked")) { - _opts.configPanel.slideDown(); - } - else { - _opts.configPanel.slideUp(); - } - }); - - _opts.createListViewButton.click(function (event) { - event.preventDefault(); - - var data = { - parentId: -1, - id: 0, - preValues: [], - action: "SaveNew", - name: "List View - " + _opts.contentTypeAlias, - selectedEditor: "Umbraco.ListView" - }; - - $.post(_opts.dataTypeServiceBaseUrl + "PostSave", JSON.stringify(data), function (result) { - _model.isSystem = result.isSystem; - _model.listViewName = result.name; - _model.listViewId = result.id; - updateElements(); - }); - }); - - _opts.removeListViewButton.click(function (event) { - event.preventDefault(); - - $.post(_opts.dataTypeServiceBaseUrl + "DeleteById?id=" + _model.listViewId, function (result) { - //re-get the data - populateData(); - }); - }); - - populateData(); - - } - }); -})(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js index d9b1a28505..e3a3ee4bf2 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditTemplate.js @@ -7,14 +7,14 @@ _opts: null, _openMacroModal: function(alias) { - + var self = this; UmbClientMgr.openAngularModalWindow({ template: "views/common/dialogs/insertmacro.html", dialogData: { renderingEngine: "WebForms", - selectedAlias: alias + macroData: { macroAlias: alias } }, callback: function(data) { UmbEditor.Insert(data.syntax, '', self._opts.editorClientId); @@ -66,7 +66,7 @@ constructor: function(opts) { // Merge options with default this._opts = $.extend({ - + // Default options go here }, opts); @@ -82,7 +82,7 @@ event.preventDefault(); self.doSubmit(); }); - + $("#sb").click(function() { self._insertCodeBlock(); }); @@ -112,7 +112,7 @@ }); }, - doSubmit: function() { + doSubmit: function() { this.save(jQuery('#' + this._opts.templateNameClientId).val(), jQuery('#' + this._opts.templateAliasClientId).val(), UmbEditor.GetCode()); }, @@ -134,7 +134,7 @@ self.submitFailure(e.message, e.header); } }); - + }, submitSuccess: function (args) { @@ -151,7 +151,10 @@ if (args.contents) { UmbEditor.SetCode(args.contents); } - + + var alias = args.alias; + this._opts.aliasTxtBox.val(alias); + top.UmbSpeechBubble.ShowMessage('save', header, msg); UmbClientMgr.mainTree().setActiveTreeType('templates'); if (pathChanged) { @@ -167,7 +170,7 @@ top.UmbSpeechBubble.ShowMessage('error', header, err); } }); - + //Set defaults for jQuery ajax calls. $.ajaxSetup({ dataType: 'json', diff --git a/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js b/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js index 28398d081a..a93f4cf815 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js +++ b/src/Umbraco.Web.UI/umbraco_client/Editors/EditView.js @@ -33,20 +33,24 @@ insertMacroMarkup: function(alias) { /// callback used to insert the markup for a macro with no parameters - + UmbEditor.Insert("@Umbraco.RenderMacro(\"" + alias + "\")", "", this._opts.codeEditorElementId); }, - + + insertRenderBody: function() { + UmbEditor.Insert("@RenderBody()", "", this._opts.codeEditorElementId); + }, + openMacroModal: function (alias) { /// callback used to display the modal dialog to insert a macro with parameters - + var self = this; UmbClientMgr.openAngularModalWindow({ template: "views/common/dialogs/insertmacro.html", dialogData: { renderingEngine: "Mvc", - selectedAlias: alias + macroData: {macroAlias: alias} }, callback: function (data) { UmbEditor.Insert(data.syntax, '', self._opts.codeEditorElementId); @@ -54,6 +58,37 @@ }); }, + openSnippetModal: function (type) { + /// callback used to display the modal dialog to insert a macro with parameters + + var self = this; + + UmbClientMgr.openAngularModalWindow({ + template: "views/common/dialogs/template/snippet.html", + callback: function (data) { + + var code = ""; + + if (type === 'section') { + code = "\n@section " + data.name + "{\n"; + code += "\n" + + "}\n"; + } + + if (type === 'rendersection') { + if (data.required) { + code = "\n@RenderSection(\"" + data.name + "\", true)\n"; + } else { + code = "\n@RenderSection(\"" + data.name + "\", false)\n"; + } + } + + UmbEditor.Insert(code, '', self._opts.codeEditorElementId); + }, + type: type + }); + }, + openQueryModal: function () { /// callback used to display the modal dialog to insert a macro with parameters @@ -106,7 +141,7 @@ }); } else { - //saving a partial view + //saving a partial view var actionName = this._opts.editorType === "PartialViewMacro" ? "SavePartialViewMacro" : "SavePartialView"; $.post(self._opts.restServiceLocation + actionName, @@ -124,9 +159,9 @@ }); } }, - + submitSuccess: function (args) { - + var msg = args.message; var header = args.header; var path = this._opts.treeSyncPath; @@ -139,12 +174,18 @@ } if (args.contents) { UmbEditor.SetCode(args.contents); + } else if (!this.IsSimpleEditor) { + // Restore focuse to text region. SetCode also does this. + UmbEditor._editor.focus(); } UmbClientMgr.mainTree().setActiveTreeType(this._opts.currentTreeType); if (this._opts.editorType == "Template") { + var alias = args.alias; + this._opts.aliasTxtBox.val(alias); + top.UmbSpeechBubble.ShowMessage('save', header, msg); //templates are different because they are ID based, whereas view files are file based without a static id @@ -156,12 +197,12 @@ else { UmbClientMgr.mainTree().syncTree(path, true); } - + } else { var newFilePath = this._opts.nameTxtBox.val(); - + function trimStart(str, trim) { if (str.startsWith(trim)) { return str.substring(trim.length); @@ -181,27 +222,31 @@ var newLocation = window.location.pathname + "?" + notFileParts.join("&") + "&file=" + newFilePath; UmbClientMgr.contentFrame(newLocation); - + //we need to do this after we navigate otherwise the navigation will wait unti lthe message timeout is done! top.UmbSpeechBubble.ShowMessage('save', header, msg); } else { - + top.UmbSpeechBubble.ShowMessage('save', header, msg); - this._opts.originalFileName = args.name; - this._opts.treeSyncPath = args.path; + if (args && args.name) { + this._opts.originalFileName = args.name; + } + if (args && args.path) { + this._opts.treeSyncPath = args.path; + } UmbClientMgr.mainTree().syncTree(path, true, null, newFilePath.split("/")[1]); - } + } } - + }, - + submitFailure: function (err, header) { - top.UmbSpeechBubble.ShowMessage('error', header, err); + top.UmbSpeechBubble.ShowMessage('error', header, err); }, - + changeMasterPageFile: function ( ) { //var editor = document.getElementById(this._opts.sourceEditorId); var templateDropDown = this._opts.masterPageDropDown.get(0); @@ -237,7 +282,7 @@ $.ajaxSetup({ dataType: 'json', cache: false, - contentType: 'application/json; charset=utf-8' + contentType: 'application/json; charset=utf-8' }); })(jQuery); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/GenericProperty/genericProperty.js b/src/Umbraco.Web.UI/umbraco_client/GenericProperty/genericProperty.js deleted file mode 100644 index 4ca59e82de..0000000000 --- a/src/Umbraco.Web.UI/umbraco_client/GenericProperty/genericProperty.js +++ /dev/null @@ -1,53 +0,0 @@ -var activeDragId = ""; -function expandCollapse(theId) { - - var edit = document.getElementById("edit" + theId); - - if (edit.style.display == 'none') { - edit.style.display = 'block'; - document.getElementById("desc" + theId).style.display = 'none'; - } - else { - edit.style.display = 'none'; - document.getElementById("desc" + theId).style.display = 'block'; - } -} -function duplicatePropertyNameAsSafeAlias(propertySelector) { - $(propertySelector).each(function() { - var prop = $(this); - var inputName = prop.find('.prop-name'); - var inputAlias = prop.find('.prop-alias'); - inputName.on('input blur', function (event) { - getSafeAlias(inputAlias, inputName.val(), false, function (alias) { - if (!inputAlias.data('dirty')) - inputAlias.val(alias); - }); - }); - inputAlias.on('input', function(event) { - inputName.off('input blur'); - }); - }); -} - -function checkAlias(aliasSelector) { - $(aliasSelector).on('input', function (event) { - var input = $(this); - input.data('dirty', true); - var value = input.val(); - validateSafeAlias(input, value, false, function (isSafe) { - input.toggleClass('highlight-error', !isSafe); - }); - }).on('blur', function(event) { - var input = $(this); - if (!input.data('dirty')) return; - input.removeData('dirty'); - var value = input.val(); - getSafeAlias(input, value, true, function (alias) { - if (value.toLowerCase() != alias.toLowerCase()) - input.val(alias); - input.removeClass('highlight-error'); - }); - }); -} - -// validateSafeAlias and getSafeAlias are defined by UmbracoCasingRules.aspx diff --git a/src/Umbraco.Web.UI/umbraco_client/GenericProperty/genericproperty.css b/src/Umbraco.Web.UI/umbraco_client/GenericProperty/genericproperty.css deleted file mode 100644 index 5183a9bccd..0000000000 --- a/src/Umbraco.Web.UI/umbraco_client/GenericProperty/genericproperty.css +++ /dev/null @@ -1,107 +0,0 @@ - -.genericPropertyForm { - -} -.genericPropertyForm h2 { - font-size: 16px; - line-height: 20px; - margin-bottom: 2px; -} - -.genericPropertyList .header{ - padding: 4px; -} -.genericPropertyList .umb-pane{ - margin: 10px; -} - -.genericPropertyList { - margin: 0px 0px 30px 0px; - list-style: none; -} - -.genericPropertyList .delete-button, .genericPropertyList .toggle-button{ - float: right; - margin-left: 3px; - border: none; - background: none; - text-decoration: none; -} - -.genericPropertyList .delete-button i { - background: none; - border: none; -} - -.handle{ - color: #ccc; - font-size: 12px; -} - -.addNewProperty .handle { - display: none; -} - -.genericPropertyList li { - display: block; - position: relative; - border-bottom: 1px solid #eee; - margin-bottom: 3px; - cursor: move; -} - -.genericPropertyList li .delete-button i { - color: #b94a48; -} - -.addNewProperty li { - cursor: default; -} - -.genericPropertyList li table { - display: block; - margin-top: 10px; -} - -.genericPropertyList li table th { - width: 140px; - padding: 5px; -} - -.propertyFormInput { - width: 300px; - z-index: 9999; -} - -.propertyForm SELECT { - width: 300px; -} - -.propertyForm h3 a { - color: #000; - text-decoration: none; - font-size: 14px; - font-weight: normal; -} - - - -.genericPropertyList li img { - background: red; - z-index: 9999; - position: relative; - border-right: medium none; - border-top: medium none; - border-left: medium none; - margin-right: 5px; - border-bottom: medium none; -} -.genericPropertyList li h3 input { - position: relative; -} - -.aliasValidationError -{ - background-color: #ff9999; - border: 1px solid: red; -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index dfbf72173e..9ad3174a28 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -4,15 +4,15 @@ @@ -89,7 +89,7 @@ - + @@ -97,16 +97,214 @@ + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -116,6 +314,14 @@ + + + + + + + + diff --git a/src/Umbraco.Web.UI/web.Template.Release.config b/src/Umbraco.Web.UI/web.Template.Release.config index 3ddea3b6a6..2b1f0d12d9 100644 --- a/src/Umbraco.Web.UI/web.Template.Release.config +++ b/src/Umbraco.Web.UI/web.Template.Release.config @@ -10,8 +10,6 @@ --> - - diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 221aecfbd0..dd66460408 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -1,309 +1,430 @@ - -
    -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - + +
    +
    +
    +
    +
    +
    - - - - - - + +
    +
    +
    +
    + + - - - - - - + + + + + + - - - - - - - - - - - + + + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - + + - - - - - - - + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - + + - - + + - - + + - - + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - + - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - diff --git a/src/Umbraco.Web/ApplicationContextExtensions.cs b/src/Umbraco.Web/ApplicationContextExtensions.cs index fcbcab1628..b91e73df0d 100644 --- a/src/Umbraco.Web/ApplicationContextExtensions.cs +++ b/src/Umbraco.Web/ApplicationContextExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.IO; +using System.Threading; using System.Web; using Umbraco.Core; @@ -7,24 +6,25 @@ namespace Umbraco.Web { public static class ApplicationContextExtensions { - /// - /// This will restart the application pool - /// - /// - /// - public static void RestartApplicationPool(this ApplicationContext appContext, HttpContextBase http) - { + /// + /// Restarts the application pool by unloading the application domain. + /// + /// + /// + public static void RestartApplicationPool(this ApplicationContext appContext, HttpContextBase http) + { + // we're going to put an application wide flag to show that the application is about to restart. + // we're doing this because if there is a script checking if the app pool is fully restarted, then + // it can check if this flag exists... if it does it means the app pool isn't restarted yet. + http.Application.Add("AppPoolRestarting", true); - //we're going to put an application wide flag to show that the application is about to restart. - //we're doing this because if there is a script checking if the app pool is fully restarted, then - //it can check if this flag exists... if it does it means the app pool isn't restarted yet. - http.Application.Add("AppPoolRestarting", true); - - //NOTE: this real way only works in full trust :( - //HttpRuntime.UnloadAppDomain(); - //so we'll do the dodgy hack instead - var configPath = http.Request.PhysicalApplicationPath + "\\web.config"; - File.SetLastWriteTimeUtc(configPath, DateTime.UtcNow); + // unload app domain - we must null out all identities otherwise we get serialization errors + // http://www.zpqrtbnk.net/posts/custom-iidentity-serialization-issue + http.User = null; + if (HttpContext.Current != null) + HttpContext.Current.User = null; + Thread.CurrentPrincipal = null; + HttpRuntime.UnloadAppDomain(); } } } diff --git a/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs b/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs index 8b742b1b2c..5c1ec9e1d4 100644 --- a/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ApplicationCacheRefresher.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); base.RefreshAll(); } @@ -38,7 +38,7 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationsCacheKey); base.Remove(id); } diff --git a/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs b/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs index e267b441a2..2f1ab4b891 100644 --- a/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ApplicationTreeCacheRefresher.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); base.RefreshAll(); } @@ -38,7 +38,7 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ApplicationTreeCacheKey); base.Remove(id); } diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index c552fb6a3b..15e01fd430 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -125,8 +125,9 @@ namespace Umbraco.Web.Cache //public access events PublicAccessService.Saved += PublicAccessService_Saved; + PublicAccessService.Deleted += PublicAccessService_Deleted; ; } - + #region Publishing void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) @@ -211,6 +212,11 @@ namespace Umbraco.Web.Cache DistributedCache.Instance.RefreshPublicAccess(); } + private void PublicAccessService_Deleted(IPublicAccessService sender, DeleteEventArgs e) + { + DistributedCache.Instance.RefreshPublicAccess(); + } + #endregion #region Content service event handlers diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 502e215c90..44a6efe9ff 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -126,8 +126,12 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); //all property type cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.PropertyTypeCacheKey); @@ -179,7 +183,7 @@ namespace Umbraco.Web.Cache /// - InMemoryCacheProvider.Current.Clear(); /// - RoutesCache.Clear(); /// - private static void ClearContentTypeCache(JsonPayload[] payloads) + private void ClearContentTypeCache(JsonPayload[] payloads) { var needsContentRefresh = false; @@ -214,18 +218,18 @@ namespace Umbraco.Web.Cache { if (payloads.Any(x => x.Type == typeof (IContentType).Name)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } if (payloads.Any(x => x.Type == typeof(IMediaType).Name)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } if (payloads.Any(x => x.Type == typeof(IMemberType).Name)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); } @@ -291,7 +295,7 @@ namespace Umbraco.Web.Cache ///
    /// true if the entity was deleted, false if it is just an update /// - private static void ClearContentTypeCache(bool isDeleted, params int[] ids) + private void ClearContentTypeCache(bool isDeleted, params int[] ids) { ClearContentTypeCache( ids.Select( diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 86114d5f77..11b3ab6294 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -92,27 +92,22 @@ namespace Umbraco.Web.Cache // db data type to store the value against and anytime a datatype changes, this also might change // we basically need to clear all sorts of runtime caches here because so many things depend upon a data type - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); payloads.ForEach(payload => { - //clear both the Id and Unique Id cache since we cache both in the legacy classes :( - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}{1}", CacheKeys.DataTypeCacheKey, payload.Id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}{1}", CacheKeys.DataTypeCacheKey, payload.UniqueId)); - //clears the prevalue cache - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id)); - + var dataTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (dataTypeCache) + dataTypeCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id)); + PublishedContentType.ClearDataType(payload.Id); }); diff --git a/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs b/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs index 8209d247a2..ae36be33df 100644 --- a/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DictionaryCacheRefresher.cs @@ -28,13 +28,13 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } } diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index 9788618bb5..6848ce2496 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using Umbraco.Core; using Umbraco.Core.Sync; @@ -37,6 +38,9 @@ namespace Umbraco.Web.Cache public const string ContentTypeCacheRefresherId = "6902E22C-9C10-483C-91F3-66B7CAE9E2F5"; public const string LanguageCacheRefresherId = "3E0F95D8-0BE5-44B8-8394-2B8750B62654"; public const string DomainCacheRefresherId = "11290A79-4B57-4C99-AD72-7748A3CF38AF"; + + [Obsolete("This is no longer used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string StylesheetCacheRefresherId = "E0633648-0DEB-44AE-9A48-75C3A55CB670"; public const string StylesheetPropertyCacheRefresherId = "2BC7A3A4-6EB1-4FBC-BAA3-C9E7B6D36D38"; public const string DataTypeCacheRefresherId = "35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2"; @@ -192,24 +196,27 @@ namespace Umbraco.Web.Cache public void RefreshAll(Guid factoryGuid) { if (factoryGuid == Guid.Empty) return; - RefreshAll(factoryGuid, true); + + ServerMessengerResolver.Current.Messenger.PerformRefreshAll( + ServerRegistrarResolver.Current.Registrar.Registrations, + GetRefresherById(factoryGuid)); } - /// - /// Notifies the distributed cache of a global invalidation for a specified . - /// - /// The unique identifier of the ICacheRefresher. - /// If true, all servers in the load balancing environment are notified; otherwise, - /// only the local server is notified. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is no longer in use and does not work as advertised, the allServers parameter doesnt have any affect for database server messengers, do not use!")] public void RefreshAll(Guid factoryGuid, bool allServers) { if (factoryGuid == Guid.Empty) return; - - ServerMessengerResolver.Current.Messenger.PerformRefreshAll( - allServers - ? ServerRegistrarResolver.Current.Registrar.Registrations - : Enumerable.Empty(), //this ensures it will only execute against the current server - GetRefresherById(factoryGuid)); + if (allServers) + { + RefreshAll(factoryGuid); + } + else + { + ServerMessengerResolver.Current.Messenger.PerformRefreshAll( + Enumerable.Empty(), + GetRefresherById(factoryGuid)); + } } /// diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 84ed6f225e..750872d8af 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Models; using umbraco; using umbraco.cms.businesslogic.web; +using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Web.Cache { @@ -262,8 +263,8 @@ namespace Umbraco.Web.Cache public static void ClearAllMacroCacheOnCurrentServer(this DistributedCache dc) { - // NOTE: The 'false' ensure that it will only refresh on the current server, not post to all servers - dc.RefreshAll(DistributedCache.MacroCacheRefresherGuid, false); + var macroRefresher = CacheRefreshersResolver.Current.GetById(DistributedCache.MacroCacheRefresherGuid); + macroRefresher.RefreshAll(); } public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) @@ -400,6 +401,12 @@ namespace Umbraco.Web.Cache dc.Remove(DistributedCache.DomainCacheRefresherGuid, domain.Id); } + public static void ClearDomainCacheOnCurrentServer(this DistributedCache dc) + { + var domainRefresher = CacheRefreshersResolver.Current.GetById(DistributedCache.DomainCacheRefresherGuid); + domainRefresher.RefreshAll(); + } + #endregion #region Language Cache @@ -435,7 +442,7 @@ namespace Umbraco.Web.Cache public static void ClearXsltCacheOnCurrentServer(this DistributedCache dc) { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration <= 0) return; - ApplicationContext.Current.ApplicationCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); } #endregion diff --git a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs index 51a2c79b2d..d19d4f6ea0 100644 --- a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs @@ -28,6 +28,12 @@ namespace Umbraco.Web.Cache get { return "Domain cache refresher"; } } + public override void RefreshAll() + { + ClearCache(); + base.RefreshAll(); + } + public override void Refresh(int id) { ClearCache(); @@ -41,8 +47,8 @@ namespace Umbraco.Web.Cache } private void ClearCache() - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + { + ClearAllIsolatedCacheByEntityType(); // SD: we need to clear the routes cache here! // diff --git a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs index 019be66b15..087aa36764 100644 --- a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs @@ -28,13 +28,15 @@ namespace Umbraco.Web.Cache public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + //if a language is removed, then all dictionary cache needs to be removed + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } } diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index a06501032f..cb62dbeb39 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -176,7 +176,7 @@ namespace Umbraco.Web.Cache prefix => ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(prefix)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.RefreshAll(); } @@ -191,7 +191,11 @@ namespace Umbraco.Web.Cache alias => ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(alias)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + var macroRepoCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (macroRepoCache) + { + macroRepoCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + } }); base.Refresh(jsonPayload); diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index dce512b1dc..a0e037e110 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Cache public class MediaCacheRefresher : JsonCacheRefresherBase { #region Static helpers - + /// /// Converts the json to a JsonPayload object /// @@ -143,13 +143,13 @@ namespace Umbraco.Web.Cache } public override void Remove(int id) - { + { ClearCache(FromMedia(ApplicationContext.Current.Services.MediaService.GetById(id), //NOTE: we'll just default to trashed for this one. OperationType.Trashed)); base.Remove(id); } - + private static void ClearCache(params JsonPayload[] payloads) { if (payloads == null) return; @@ -159,40 +159,40 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.ClearPartialViewCache(); payloads.ForEach(payload => + { + var mediaCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + + //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) + if (payload.Path.IsNullOrWhiteSpace()) { - - //if there's no path, then just use id (this will occur on permanent deletion like emptying recycle bin) - if (payload.Path.IsNullOrWhiteSpace()) + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); + } + else + { + foreach (var idPart in payload.Path.Split(',')) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - } - else - { - foreach (var idPart in payload.Path.Split(',')) + int idPartAsInt; + if (int.TryParse(idPart, out idPartAsInt) && mediaCache) { - int idPartAsInt; - if (int.TryParse(idPart, out idPartAsInt)) - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem( - RepositoryBase.GetCacheIdKey(idPartAsInt)); - } + mediaCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(idPartAsInt)); + } + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( + string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); + + // Also clear calls that only query this specific item! + if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}_True", CacheKeys.MediaCacheKey, idPart)); - - // Also clear calls that only query this specific item! - if (idPart == payload.Id.ToString(CultureInfo.InvariantCulture)) - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch( - string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); - } + string.Format("{0}_{1}", CacheKeys.MediaCacheKey, payload.Id)); } + } + + // published cache... + PublishedMediaCache.ClearCache(payload.Id); + }); - // published cache... - PublishedMediaCache.ClearCache(payload.Id); - }); - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index a0167454a5..32e6a69717 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -69,7 +69,9 @@ namespace Umbraco.Web.Cache ApplicationContext.Current.ApplicationCache.RuntimeCache. ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.MemberBusinessLogicCacheKey, id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + var memberCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (memberCache) + memberCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs index e410ab560c..dc2ba39b9d 100644 --- a/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberGroupCacheRefresher.cs @@ -104,13 +104,13 @@ namespace Umbraco.Web.Cache { if (payloads == null) return; + var memberGroupCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); payloads.ForEach(payload => { - if (payload != null) + if (payload != null && memberGroupCache) { - ApplicationContext.Current.ApplicationCache.RuntimeCache - .ClearCacheByKeySearch(string.Format("{0}.{1}", typeof(IMemberGroup).FullName, payload.Name)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + memberGroupCache.Result.ClearCacheByKeySearch(string.Format("{0}.{1}", typeof(IMemberGroup).FullName, payload.Name)); + memberGroupCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); } }); diff --git a/src/Umbraco.Web/Cache/PageCacheRefresher.cs b/src/Umbraco.Web/Cache/PageCacheRefresher.cs index 73f525383e..a8e24009fb 100644 --- a/src/Umbraco.Web/Cache/PageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PageCacheRefresher.cs @@ -76,7 +76,7 @@ namespace Umbraco.Web.Cache content.Instance.ClearDocumentCache(id); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } @@ -86,7 +86,7 @@ namespace Umbraco.Web.Cache content.Instance.UpdateDocumentCache(new Document(instance)); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(instance); } @@ -96,7 +96,7 @@ namespace Umbraco.Web.Cache content.Instance.ClearDocumentCache(new Document(instance)); DistributedCache.Instance.ClearAllMacroCacheOnCurrentServer(); DistributedCache.Instance.ClearXsltCacheOnCurrentServer(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(instance); } } diff --git a/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs b/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs index 09b38f1ac3..922e01e2a2 100644 --- a/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/PublicAccessCacheRefresher.cs @@ -27,25 +27,25 @@ namespace Umbraco.Web.Cache public override void Refresh(Guid id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Refresh(id); } public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.RefreshAll(); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } } diff --git a/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs b/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs index 5d98671a76..959a937e7b 100644 --- a/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/StylesheetCacheRefresher.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Umbraco.Core; using Umbraco.Core.Cache; @@ -7,6 +8,8 @@ namespace Umbraco.Web.Cache /// /// A cache refresher to ensure stylesheet cache is refreshed when stylesheets change /// + [Obsolete("This is no longer used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class StylesheetCacheRefresher : CacheRefresherBase { protected override StylesheetCacheRefresher Instance @@ -24,27 +27,5 @@ namespace Umbraco.Web.Cache get { return "Stylesheet cache refresher"; } } - public override void RefreshAll() - { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.StylesheetCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetCacheKey(id)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetCacheKey(id)); - base.Remove(id); - } - - private static string GetStylesheetCacheKey(int id) - { - return CacheKeys.StylesheetCacheKey + id; - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs b/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs index d7006c8531..5b72e0384b 100644 --- a/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/StylesheetPropertyCacheRefresher.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Umbraco.Core; using Umbraco.Core.Cache; @@ -7,6 +8,8 @@ namespace Umbraco.Web.Cache /// /// A cache refresher to ensure stylesheet property cache is refreshed when stylesheet properties change /// + [Obsolete("This is no longer used and will be removed in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class StylesheetPropertyCacheRefresher : CacheRefresherBase { protected override StylesheetPropertyCacheRefresher Instance @@ -23,28 +26,6 @@ namespace Umbraco.Web.Cache { get { return "Stylesheet property cache refresher"; } } - - public override void RefreshAll() - { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.StylesheetPropertyCacheKey); - base.RefreshAll(); - } - - public override void Refresh(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetPropertyCacheKey(id)); - base.Refresh(id); - } - - public override void Remove(int id) - { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(GetStylesheetPropertyCacheKey(id)); - base.Remove(id); - } - - private static string GetStylesheetPropertyCacheKey(int id) - { - return CacheKeys.StylesheetPropertyCacheKey + id; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs index 22bce3d5bf..4d6c0c612b 100644 --- a/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/TemplateCacheRefresher.cs @@ -52,8 +52,8 @@ namespace Umbraco.Web.Cache // all three of these types are referenced by templates, and the cache needs to be cleared on every server, // otherwise things like looking up content type's after a template is removed is still going to show that // it has an associated template. - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); base.Remove(id); } @@ -66,7 +66,7 @@ namespace Umbraco.Web.Cache string.Format("{0}{1}", CacheKeys.TemplateFrontEndCacheKey, id)); //need to clear the runtime cache for templates - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); } } diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index 47a2d73bdb..beb5b8db7d 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -77,39 +77,44 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); + ClearAllIsolatedCacheByEntityType(); + DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.RefreshAll(); } public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(id); + ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(id); + DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(id); + ClearAllIsolatedCacheByEntityType(); + DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Remove(id); } public override void Refresh(IContent instance) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(instance.Id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(instance.Id); + ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(instance); + DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Refresh(instance); } public override void Remove(IContent instance) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(instance.Id)); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearRepositoryCacheItemById(instance.Id); + ClearAllIsolatedCacheByEntityType(); + DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); base.Remove(instance); } @@ -119,16 +124,27 @@ namespace Umbraco.Web.Cache /// public void Refresh(string jsonPayload) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); foreach (var payload in DeserializeFromJsonPayload(jsonPayload)) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(payload.Id)); + ClearRepositoryCacheItemById(payload.Id); content.Instance.UpdateSortOrder(payload.Id); } + DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); + OnCacheUpdated(Instance, new CacheRefresherEventArgs(jsonPayload, MessageType.RefreshByJson)); } + + private void ClearRepositoryCacheItemById(int id) + { + var contentCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (contentCache) + { + contentCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs index 95a7fdac49..6f9cc7db80 100644 --- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -30,9 +30,9 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.UserContextCacheKey); + ClearAllIsolatedCacheByEntityType(); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); base.RefreshAll(); } @@ -44,16 +44,20 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); - - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); - - //we need to clear all UserContextCacheKey since we cannot invalidate based on ID since the cache is done so based - //on the current contextId stored in the database - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.UserContextCacheKey); + var userCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (userCache) + userCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); + base.Remove(id); } + private Attempt UserPermissionsCache + { + get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs b/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs index db1baf1bcd..eed29ff764 100644 --- a/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserPermissionsCacheRefresher.cs @@ -1,6 +1,7 @@ using System; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Cache { @@ -31,7 +32,8 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheByKeySearch(CacheKeys.UserPermissionsCacheKey); base.RefreshAll(); } @@ -43,8 +45,14 @@ namespace Umbraco.Web.Cache public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.ClearCacheItem(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); + if (UserPermissionsCache) + UserPermissionsCache.Result.ClearCacheByKeySearch(string.Format("{0}{1}", CacheKeys.UserPermissionsCacheKey, id)); base.Remove(id); } + + private Attempt UserPermissionsCache + { + get { return ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs index 2beaa4347d..ca3f93c068 100644 --- a/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserTypeCacheRefresher.cs @@ -29,19 +29,23 @@ namespace Umbraco.Web.Cache public override void RefreshAll() { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheObjectTypes(); + ClearAllIsolatedCacheByEntityType(); base.RefreshAll(); } public override void Refresh(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + var userTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (userTypeCache) + userTypeCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); base.Refresh(id); } public override void Remove(int id) { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + var userTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (userTypeCache) + userTypeCache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); base.Remove(id); } diff --git a/src/Umbraco.Web/CacheHelperExtensions.cs b/src/Umbraco.Web/CacheHelperExtensions.cs index f23bb6d070..1b0451a999 100644 --- a/src/Umbraco.Web/CacheHelperExtensions.cs +++ b/src/Umbraco.Web/CacheHelperExtensions.cs @@ -58,7 +58,7 @@ namespace Umbraco.Web /// public static void ClearPartialViewCache(this CacheHelper cacheHelper) { - cacheHelper.ClearCacheByKeySearch(PartialViewCacheKey); + cacheHelper.RuntimeCache.ClearCacheByKeySearch(PartialViewCacheKey); } } } diff --git a/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs b/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs index 29954701ea..fdb4145160 100644 --- a/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs +++ b/src/Umbraco.Web/Dictionary/UmbracoCultureDictionary.cs @@ -18,6 +18,11 @@ namespace Umbraco.Web.Dictionary /// /// A culture dictionary that uses the Umbraco ILocalizationService /// + /// + /// TODO: The ICultureDictionary needs to represent the 'fast' way to do dictionary item retrieval - for front-end and back office. + /// The ILocalizationService is the service used for interacting with this data from the database which isn't all that fast + /// (even though there is caching involved, if there's lots of dictionary items the caching is not great) + /// public class DefaultCultureDictionary : Umbraco.Core.Dictionary.ICultureDictionary { private readonly ILocalizationService _localizationService; @@ -93,6 +98,11 @@ namespace Umbraco.Web.Dictionary /// /// /// + /// + /// NOTE: The result of this is not cached anywhere - the underlying repository does not cache + /// the child lookups because that is done by a query lookup. This method isn't used in our codebase + /// so I don't think this is a performance issue but if devs are using this it could be optimized here. + /// public IDictionary GetChildren(string key) { var result = new Dictionary(); @@ -126,6 +136,7 @@ namespace Umbraco.Web.Dictionary get { //ensure it's stored/retrieved from request cache + //NOTE: This is no longer necessary since these are cached at the runtime level, but we can leave it here for now. return _requestCacheProvider.GetCacheItem(typeof (DefaultCultureDictionary).Name + "Culture", () => _localizationService.GetLanguageByIsoCode(Culture.Name)); } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 7f7b59199a..1cfd0312c2 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -3,41 +3,31 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; -using System.Security.Claims; -using System.ServiceModel.Channels; -using System.Text; using System.Threading.Tasks; using System.Web; -using System.Web.Helpers; using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Security; +using System.Web.Mvc; using AutoMapper; using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; -using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Membership; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Security; +using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; -using Umbraco.Core.Security; using Umbraco.Web.Security; +using Umbraco.Web.Security.Identity; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; -using umbraco.providers; -using Microsoft.AspNet.Identity.Owin; -using Umbraco.Core.Logging; -using Newtonsoft.Json.Linq; -using Umbraco.Core.Models.Identity; -using Umbraco.Web.Security.Identity; using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.Editors { - /// /// The API controller used for editing content /// @@ -97,7 +87,7 @@ namespace Umbraco.Web.Editors if (result.Succeeded) { var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); - await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + await SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false); return Request.CreateResponse(HttpStatusCode.OK); } else @@ -111,7 +101,7 @@ namespace Umbraco.Web.Editors /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) ///
    /// - [HttpGet] + [System.Web.Http.HttpGet] public bool IsAuthenticated() { var attempt = UmbracoContext.Security.AuthorizeRequest(); @@ -163,9 +153,7 @@ namespace Umbraco.Web.Editors [SetAngularAntiForgeryTokens] public async Task PostLogin(LoginModel loginModel) { - var http = this.TryGetHttpContext(); - if (http.Success == false) - throw new InvalidOperationException("This method requires that an HttpContext be active"); + var http = EnsureHttpContext(); var result = await SignInManager.PasswordSignInAsync( loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); @@ -177,19 +165,14 @@ namespace Umbraco.Web.Editors //get the user var user = Security.GetBackOfficeUser(loginModel.Username); var userDetail = Mapper.Map(user); - + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; + //create a response with the userDetail object var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - //set the response cookies with the ticket (NOTE: This needs to be done with the custom webapi extension because - // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) - var ticket = response.UmbracoLoginWebApi(user); - - //This ensure the current principal is set, otherwise any logic executing after this wouldn't actually be authenticated - http.Result.AuthenticateCurrentRequest(ticket, false); - - //update the userDetail and set their remaining seconds - userDetail.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); + //ensure the user is set for the current request + Request.SetPrincipalForRequest(user); return response; @@ -236,16 +219,104 @@ namespace Umbraco.Web.Editors } } + /// + /// Processes a password reset request. Looks for a match on the provided email address + /// and if found sends an email with a link to reset it + /// + /// + [SetAngularAntiForgeryTokens] + public async Task PostRequestPasswordReset(RequestPasswordResetModel model) + { + // If this feature is switched off in configuration the UI will be amended to not make the request to reset password available. + // So this is just a server-side secondary check. + if (UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset == false) + { + throw new HttpResponseException(HttpStatusCode.BadRequest); + } + var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email); + if (identityUser != null) + { + var user = Services.UserService.GetByEmail(model.Email); + if (user != null && user.IsLockedOut == false) + { + var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); + var callbackUrl = ConstuctCallbackUrl(identityUser.Id, code); + + var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", + //Ensure the culture of the found user is used for the email! + UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService), + new[] {identityUser.UserName, callbackUrl}); + + await UserManager.SendEmailAsync(identityUser.Id, + Services.TextService.Localize("login/resetPasswordEmailCopySubject", + //Ensure the culture of the found user is used for the email! + UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService)), + message); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + private string ConstuctCallbackUrl(int userId, string code) + { + //get an mvc helper to get the url + var http = EnsureHttpContext(); + var urlHelper = new UrlHelper(http.Request.RequestContext); + + var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", + new + { + area = GlobalSettings.UmbracoMvcArea, + u = userId, + r = code + }); + + //TODO: Virtual path? + + return string.Format("{0}://{1}{2}", + http.Request.Url.Scheme, + http.Request.Url.Host + (http.Request.Url.Port == 80 ? string.Empty : ":" + http.Request.Url.Port), + action); + } + + /// + /// Processes a set password request. Validates the request and sets a new password. + /// + /// + [SetAngularAntiForgeryTokens] + public async Task PostSetPassword(SetPasswordModel model) + { + var result = await UserManager.ResetPasswordAsync(model.UserId, model.ResetCode, model.Password); + if (result.Succeeded) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + return Request.CreateValidationErrorResponse( + result.Errors.Any() ? result.Errors.First() : "Set password failed"); + } + + private HttpContextBase EnsureHttpContext() + { + var attempt = this.TryGetHttpContext(); + if (attempt.Success == false) + throw new InvalidOperationException("This method requires that an HttpContext be active"); + return attempt.Result; + } /// /// Logs the current user out /// /// - [UmbracoBackOfficeLogout] [ClearAngularAntiForgeryToken] [ValidateAngularAntiForgeryToken] public HttpResponseMessage PostLogout() { + Request.TryGetOwinContext().Result.Authentication.SignOut( + Core.Constants.Security.BackOfficeAuthenticationType, + Core.Constants.Security.BackOfficeExternalAuthenticationType); + Logger.Info("User {0} from IP address {1} has logged out", () => User.Identity == null ? "UNKNOWN" : User.Identity.Name, () => TryGetOwinContext().Result.Request.RemoteIpAddress); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 689f859462..95e1f7803e 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -1,11 +1,16 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.Globalization; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Web; +using System.Web.Configuration; using System.Web.Mvc; using System.Web.UI; using ClientDependency.Core.Config; @@ -23,6 +28,7 @@ using Umbraco.Core.Manifest; using Umbraco.Core.Models; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; +using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.PropertyEditors; @@ -31,6 +37,7 @@ using Umbraco.Web.Trees; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi.Filters; using Umbraco.Web.WebServices; +using Umbraco.Core.Services; using Action = umbraco.BusinessLogic.Actions.Action; using Constants = Umbraco.Core.Constants; @@ -46,6 +53,10 @@ namespace Umbraco.Web.Editors private BackOfficeUserManager _userManager; private BackOfficeSignInManager _signInManager; + private const string TokenExternalSignInError = "ExternalSignInError"; + private const string TokenPasswordResetCode = "PasswordResetCode"; + private static readonly string[] TempDataTokenNames = { TokenExternalSignInError, TokenPasswordResetCode }; + protected BackOfficeSignInManager SignInManager { get { return _signInManager ?? (_signInManager = OwinContext.Get()); } @@ -98,7 +109,8 @@ namespace Umbraco.Web.Editors var cultureInfo = string.IsNullOrWhiteSpace(culture) //if the user is logged in, get their culture, otherwise default to 'en' ? Security.IsAuthenticated() - ? Security.CurrentUser.GetUserCulture(Services.TextService) + //current culture is set at the very beginning of each request + ? Thread.CurrentThread.CurrentCulture : CultureInfo.GetCultureInfo("en") : CultureInfo.GetCultureInfo(culture); @@ -177,6 +189,13 @@ namespace Umbraco.Web.Editors return new JsonNetResult { Data = gridConfig.EditorsConfig.Editors, Formatting = Formatting.Indented }; } + private string GetMaxRequestLength() + { + var section = ConfigurationManager.GetSection("system.web/httpRuntime") as HttpRuntimeSection; + if (section == null) return string.Empty; + return section.MaxRequestLength.ToString(); + } + /// /// Returns the JavaScript object representing the static server variables javascript object /// @@ -272,6 +291,10 @@ namespace Umbraco.Web.Editors "logApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetEntityLog(0)) }, + { + "gravatarApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetCurrentUserGravatarUrl()) + }, { "memberApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetByKey(Guid.Empty)) @@ -340,7 +363,17 @@ namespace Umbraco.Web.Editors "imageFileTypes", string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes) }, + { + "disallowedUploadFiles", + string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles) + }, + { + "maxFileSize", + GetMaxRequestLength() + }, {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn}, + {"cssPath", IOHelper.ResolveUrl(SystemDirectories.Css).TrimEnd('/')}, + {"allowPasswordReset", UmbracoConfig.For.UmbracoSettings().Security.AllowPasswordReset}, } }, { @@ -349,7 +382,9 @@ namespace Umbraco.Web.Editors {"trees", GetTreePluginsMetaData()} } }, - {"isDebuggingEnabled", HttpContext.IsDebuggingEnabled}, + { + "isDebuggingEnabled", HttpContext.IsDebuggingEnabled + }, { "application", GetApplicationState() }, @@ -385,7 +420,7 @@ namespace Umbraco.Web.Editors return JavaScript(result); } - + [HttpPost] public ActionResult ExternalLogin(string provider, string redirectUrl = null) { @@ -408,7 +443,25 @@ namespace Umbraco.Web.Editors User.Identity.GetUserId()); } + [HttpGet] + public async Task ValidatePasswordResetCode([Bind(Prefix = "u")]int userId, [Bind(Prefix = "r")]string resetCode) + { + var user = UserManager.FindById(userId); + if (user != null) + { + var result = await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", resetCode, UserManager, user); + if (result) + { + //Add a flag and redirect for it to be displayed + TempData[TokenPasswordResetCode] = new ValidatePasswordResetCodeModel {UserId = userId, ResetCode = resetCode}; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + } + //Add error and redirect for it to be displayed + TempData[TokenPasswordResetCode] = new[] { Services.TextService.Localize("login/resetCodeExpired") }; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } [HttpGet] public async Task ExternalLinkLoginCallback() @@ -420,7 +473,7 @@ namespace Umbraco.Web.Editors if (loginInfo == null) { //Add error and redirect for it to be displayed - TempData["ExternalSignInError"] = new[] { "An error occurred, could not get external login info" }; + TempData[TokenExternalSignInError] = new[] { "An error occurred, could not get external login info" }; return RedirectToLocal(Url.Action("Default", "BackOffice")); } @@ -431,27 +484,32 @@ namespace Umbraco.Web.Editors } //Add errors and redirect for it to be displayed - TempData["ExternalSignInError"] = result.Errors; + TempData[TokenExternalSignInError] = result.Errors; return RedirectToLocal(Url.Action("Default", "BackOffice")); } /// - /// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, otherwise - /// process the external login info. + /// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, + /// otherwise process the external login info. /// - /// - private async Task RenderDefaultOrProcessExternalLoginAsync(Func defaultResponse, Func externalSignInResponse) + /// + private async Task RenderDefaultOrProcessExternalLoginAsync( + Func defaultResponse, + Func externalSignInResponse) { if (defaultResponse == null) throw new ArgumentNullException("defaultResponse"); if (externalSignInResponse == null) throw new ArgumentNullException("externalSignInResponse"); ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; - //check if there's errors in the TempData, assign to view bag and render the view - if (TempData["ExternalSignInError"] != null) - { - ViewBag.ExternalSignInError = TempData["ExternalSignInError"]; - return defaultResponse(); + //check if there is the TempData with the any token name specified, if so, assign to view bag and render the view + foreach (var tempDataTokenName in TempDataTokenNames) + { + if (TempData[tempDataTokenName] != null) + { + ViewData[tempDataTokenName] = TempData[tempDataTokenName]; + return defaultResponse(); + } } //First check if there's external login info, if there's not proceed as normal @@ -489,7 +547,7 @@ namespace Umbraco.Web.Editors { if (await AutoLinkAndSignInExternalAccount(loginInfo) == false) { - ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; } //Remove the cookie otherwise this message will keep appearing @@ -524,7 +582,7 @@ namespace Umbraco.Web.Editors //we are allowing auto-linking/creating of local accounts if (loginInfo.Email.IsNullOrWhiteSpace()) { - ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + ViewData[TokenExternalSignInError] = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; } else { @@ -533,7 +591,7 @@ namespace Umbraco.Web.Editors var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); if (foundByEmail != null) { - ViewBag.ExternalSignInError = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; + ViewData[TokenExternalSignInError] = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; } else { @@ -541,11 +599,14 @@ namespace Umbraco.Web.Editors var userType = Services.UserService.GetUserTypeByAlias(defaultUserType); if (userType == null) { - ViewBag.ExternalSignInError = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; + ViewData[TokenExternalSignInError] = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; } else { + if (loginInfo.Email.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Email value cannot be null"); + if (loginInfo.ExternalIdentity.Name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); + var autoLinkUser = new BackOfficeIdentityUser() { Email = loginInfo.Email, @@ -566,21 +627,21 @@ namespace Umbraco.Web.Editors if (userCreationResult.Succeeded == false) { - ViewBag.ExternalSignInError = userCreationResult.Errors; + ViewData[TokenExternalSignInError] = userCreationResult.Errors; } else { var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); if (linkResult.Succeeded == false) { - ViewBag.ExternalSignInError = linkResult.Errors; + ViewData[TokenExternalSignInError] = linkResult.Errors; //If this fails, we should really delete the user since it will be in an inconsistent state! var deleteResult = await UserManager.DeleteAsync(autoLinkUser); if (deleteResult.Succeeded == false) { //DOH! ... this isn't good, combine all errors to be shown - ViewBag.ExternalSignInError = linkResult.Errors.Concat(deleteResult.Errors); + ViewData[TokenExternalSignInError] = linkResult.Errors.Concat(deleteResult.Errors); } } else @@ -622,6 +683,7 @@ namespace Umbraco.Web.Editors app.Add("applicationPath", HttpContext.Request.ApplicationPath.EnsureEndsWith('/')); return app; } + private IEnumerable> GetTreePluginsMetaData() { @@ -756,12 +818,29 @@ namespace Umbraco.Web.Editors //Ensure the forms auth module doesn't do a redirect! context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; + var owinCtx = context.HttpContext.GetOwinContext(); + + //First, see if a custom challenge result callback is specified for the provider + // and use it instead of the default if one is supplied. + var loginProvider = owinCtx.Authentication + .GetExternalAuthenticationTypes() + .FirstOrDefault(p => p.AuthenticationType == LoginProvider); + if (loginProvider != null) + { + var providerChallengeResult = loginProvider.GetSignInChallengeResult(owinCtx); + if (providerChallengeResult != null) + { + owinCtx.Authentication.Challenge(providerChallengeResult, LoginProvider); + return; + } + } + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri.EnsureEndsWith('/') }; if (UserId != null) { properties.Dictionary[XsrfKey] = UserId; } - context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); + owinCtx.Authentication.Challenge(properties, LoginProvider); } } } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f8a75a23d1..7abdaf7e69 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -8,6 +8,7 @@ using System.Net.Http.Formatting; using System.Text; using System.Web.Http; using System.Web.Http.ModelBinding; +using System.Web.Http.ModelBinding.Binders; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; @@ -30,6 +31,7 @@ using Umbraco.Core.Dynamics; using umbraco.BusinessLogic.Actions; using umbraco.cms.businesslogic.web; using umbraco.presentation.preview; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.UI; using Constants = Umbraco.Core.Constants; using Notification = Umbraco.Web.Models.ContentEditing.Notification; @@ -46,13 +48,13 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] public class ContentController : ContentControllerBase - { + { /// /// Constructor /// public ContentController() - : this(UmbracoContext.Current) - { + : this(UmbracoContext.Current) + { } /// @@ -60,8 +62,8 @@ namespace Umbraco.Web.Editors /// /// public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { + : base(umbracoContext) + { } /// @@ -76,23 +78,47 @@ namespace Umbraco.Web.Editors return foundContent.Select(Mapper.Map); } + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + /// /// Gets the content json for the content id /// /// /// + [OutgoingEditorModelEvent] [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetById(int id) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); if (foundContent == null) { HandleContentNotFound(id); } - + var content = Mapper.Map(foundContent); return content; - } + } [EnsureUserPermissionForContent("id")] public ContentItemDisplay GetWithTreeDefinition(int id) @@ -116,6 +142,7 @@ namespace Umbraco.Web.Editors /// If this is a container type, we'll remove the umbContainerView tab for a new item since /// it cannot actually list children if it doesn't exist yet. /// + [OutgoingEditorModelEvent] public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) { var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); @@ -129,7 +156,7 @@ namespace Umbraco.Web.Editors //remove this tab if it exists: umbContainerView var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] {containerTab}); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); return mapped; } @@ -142,7 +169,7 @@ namespace Umbraco.Web.Editors { var url = Umbraco.NiceUrl(id); var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); return response; } @@ -152,18 +179,21 @@ namespace Umbraco.Web.Editors /// [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - string filter = "") - { + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { long totalChildren; IContent[] children; if (pageNumber > 0 && pageSize > 0) { - children = Services.ContentService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); } else { @@ -178,7 +208,7 @@ namespace Umbraco.Web.Editors var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); pagedResult.Items = children - .Select(Mapper.Map>); + .Select(Mapper.Map>); return pagedResult; } @@ -186,7 +216,20 @@ namespace Umbraco.Web.Editors [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] public bool GetHasPermission(string permissionToCheck, int nodeId) { - return HasPermission(permissionToCheck, nodeId); + return HasPermission(permissionToCheck, nodeId); + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + return Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds) + .ToDictionary(x => x.EntityId, x => x.AssignedPermissions); } [HttpGet] @@ -200,7 +243,7 @@ namespace Umbraco.Web.Editors return false; } - + /// /// Saves content /// @@ -208,16 +251,16 @@ namespace Umbraco.Web.Editors [FileUploadCleanupFilter] [ContentPostValidate] public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { //If we've reached here it means: // * Our model has been bound // * and validated // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - + MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -235,7 +278,7 @@ namespace Umbraco.Web.Editors var forDisplay = Mapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - + } //if the model state is not valid we cannot publish so change it to save @@ -252,7 +295,7 @@ namespace Umbraco.Web.Editors //initialize this to successful var publishStatus = Attempt.Succeed(); - var wasCancelled = false; + var wasCancelled = false; if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) { @@ -287,8 +330,8 @@ namespace Umbraco.Web.Editors if (wasCancelled == false) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize("speechBubbles/editContentSavedText")); + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + Services.TextService.Localize("speechBubbles/editContentSavedText")); } else { @@ -300,8 +343,8 @@ namespace Umbraco.Web.Editors if (wasCancelled == false) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); } else { @@ -327,8 +370,6 @@ namespace Umbraco.Web.Editors return display; } - - /// /// Publishes a document with a given ID /// @@ -354,7 +395,7 @@ namespace Umbraco.Web.Editors { var notificationModel = new SimpleNotificationModel(); ShowMessageForPublishStatus(publishResult.Result, notificationModel); - return Request.CreateValidationErrorResponse(notificationModel); + return Request.CreateValidationErrorResponse(notificationModel); } //return ok @@ -402,7 +443,7 @@ namespace Umbraco.Web.Editors //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } + } } return Request.CreateResponse(HttpStatusCode.OK); @@ -419,7 +460,7 @@ namespace Umbraco.Web.Editors [HttpPost] [EnsureUserPermissionForContent(Constants.System.RecycleBinContent)] public HttpResponseMessage EmptyRecycleBin() - { + { Services.ContentService.EmptyRecycleBin(); return Request.CreateResponse(HttpStatusCode.OK); } @@ -478,7 +519,7 @@ namespace Umbraco.Web.Editors var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; + return response; } /// @@ -491,7 +532,7 @@ namespace Umbraco.Web.Editors { var toCopy = ValidateMoveOrCopy(copy); - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal); + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive); var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); @@ -510,7 +551,7 @@ namespace Umbraco.Web.Editors if (foundContent == null) HandleContentNotFound(id); - + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); var content = Mapper.Map(foundContent); @@ -521,7 +562,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); } else - { + { content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); return content; } @@ -559,8 +600,8 @@ namespace Umbraco.Web.Editors contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; //only set the template if it didn't change var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); if (templateChanged) { var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); @@ -603,7 +644,8 @@ namespace Umbraco.Web.Editors if (toMove.ContentType.AllowedAsRoot == false) { throw new HttpResponseException( - Request.CreateValidationErrorResponse(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); } } else @@ -616,17 +658,19 @@ namespace Umbraco.Web.Editors //check if the item is allowed under this one if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) + .Any(x => x.Value == toMove.ContentType.Id) == false) { throw new HttpResponseException( - Request.CreateValidationErrorResponse(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); } // Check on paths if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { throw new HttpResponseException( - Request.CreateValidationErrorResponse(Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); } } @@ -640,44 +684,52 @@ namespace Umbraco.Web.Editors case PublishStatusType.Success: case PublishStatusType.SuccessAlreadyPublished: display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); break; case PublishStatusType.FailedPathNotPublished: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); break; case PublishStatusType.FailedCancelledByEvent: AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; + break; case PublishStatusType.FailedAwaitingRelease: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); break; case PublishStatusType.FailedHasExpired: - //TODO: We should add proper error messaging for this! + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; case PublishStatusType.FailedIsTrashed: //TODO: We should add proper error messaging for this! + break; case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); break; default: throw new IndexOutOfRangeException(); } } - + /// /// Performs a permissions check for the user to check if it has access to the node based on @@ -692,15 +744,15 @@ namespace Umbraco.Web.Editors /// Specifies the already resolved content item to check against /// internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) { - + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) { contentItem = contentService.GetById(nodeId); @@ -715,16 +767,16 @@ namespace Umbraco.Web.Editors } var hasPathAccess = (nodeId == Constants.System.Root) - ? UserExtensions.HasPathAccess( - Constants.System.Root.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : (nodeId == Constants.System.RecycleBinContent) - ? UserExtensions.HasPathAccess( - Constants.System.RecycleBinContent.ToInvariantString(), - user.StartContentId, - Constants.System.RecycleBinContent) - : user.HasPathAccess(contentItem); + ? UserExtensions.HasPathAccess( + Constants.System.Root.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : (nodeId == Constants.System.RecycleBinContent) + ? UserExtensions.HasPathAccess( + Constants.System.RecycleBinContent.ToInvariantString(), + user.StartContentId, + Constants.System.RecycleBinContent) + : user.HasPathAccess(contentItem); if (hasPathAccess == false) { diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 4c2ea733b6..45a2821130 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -1,40 +1,47 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; using System.Net; using System.Web.Http; using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Dictionary; using Umbraco.Core.Models; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using System.Linq; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; -using Newtonsoft.Json; +using Umbraco.Core.Services; +using Umbraco.Core.PropertyEditors; +using System.Net.Http; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Strings; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Logging; +using Umbraco.Web.Models; namespace Umbraco.Web.Editors { - - //TODO: We'll need to be careful about the security on this controller, when we start implementing + //TODO: We'll need to be careful about the security on this controller, when we start implementing // methods to modify content types we'll need to enforce security on the individual methods, we - // cannot put security on the whole controller because things like GetAllowedChildren are required for content editing. + // cannot put security on the whole controller because things like + // GetAllowedChildren, GetPropertyTypeScaffold, GetAllPropertyTypeAliases are required for content editing. /// /// An API controller used for dealing with content types /// [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + [EnableOverrideAuthorization] public class ContentTypeController : ContentTypeControllerBase { - private ICultureDictionary _cultureDictionary; - /// /// Constructor /// public ContentTypeController() : this(UmbracoContext.Current) - { + { } /// @@ -46,20 +53,214 @@ namespace Umbraco.Web.Editors { } + public int GetCount() + { + return Services.ContentTypeService.CountContentTypes(); + } + + public DocumentTypeDisplay GetById(int id) + { + var ct = Services.ContentTypeService.GetContentType(id); + if (ct == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var dto = Mapper.Map(ct); + return dto; + } + + /// + /// Deletes a document type wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundType = Services.ContentTypeService.GetContentType(id); + if (foundType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + Services.ContentTypeService.Delete(foundType, Security.CurrentUser.Id); + return Request.CreateResponse(HttpStatusCode.OK); + } /// /// Gets all user defined properties. /// /// + [UmbracoTreeAuthorize( + Constants.Trees.DocumentTypes, Constants.Trees.Content, + Constants.Trees.MediaTypes, Constants.Trees.Media, + Constants.Trees.MemberTypes, Constants.Trees.Members)] public IEnumerable GetAllPropertyTypeAliases() { return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } + /// + /// Returns the avilable compositions for this content type + /// This has been wrapped in a dto instead of simple parameters to support having multiple parameters in post request body + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + [HttpPost] + public HttpResponseMessage GetAvailableCompositeContentTypes(GetAvailableCompositionsFilter filter) + { + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.DocumentType, filter.FilterContentTypes, filter.FilterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); + } + + [UmbracoTreeAuthorize( + Constants.Trees.DocumentTypes, Constants.Trees.Content, + Constants.Trees.MediaTypes, Constants.Trees.Media, + Constants.Trees.MemberTypes, Constants.Trees.Members)] + public ContentPropertyDisplay GetPropertyTypeScaffold(int id) + { + var dataTypeDiff = Services.DataTypeService.GetDataTypeDefinitionById(id); + + if (dataTypeDiff == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var preVals = UmbracoContext.Current.Application.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(id); + var editor = PropertyEditorResolver.Current.GetByAlias(dataTypeDiff.PropertyEditorAlias); + + return new ContentPropertyDisplay() + { + Editor = dataTypeDiff.PropertyEditorAlias, + Validation = new PropertyTypeValidation() { }, + View = editor.ValueEditor.View, + Config = editor.PreValueEditor.ConvertDbToEditor(editor.DefaultPreValues, preVals) + }; + } + + /// + /// Deletes a document type container wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteContainer(int id) + { + Services.ContentTypeService.DeleteContentTypeContainer(id, Security.CurrentUser.Id); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + public HttpResponseMessage PostCreateContainer(int parentId, string name) + { + var result = Services.ContentTypeService.CreateContentTypeContainer(parentId, name, Security.CurrentUser.Id); + + return result + ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id + : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + } + + public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave) + { + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetContentType(i), + saveContentType: type => Services.ContentTypeService.Save(type), + beforeCreateNew: ctSave => + { + //create a default template if it doesnt exist -but only if default template is == to the content type + if (ctSave.DefaultTemplate.IsNullOrWhiteSpace() == false && ctSave.DefaultTemplate == ctSave.Alias) + { + var template = Services.FileService.GetTemplate(ctSave.Alias); + if (template == null) + { + var tryCreateTemplate = Services.FileService.CreateTemplateForContentType(ctSave.Alias, ctSave.Name); + if (tryCreateTemplate == false) + { + Logger.Warn( + "Could not create a template for the Content Type: {0}, status: {1}", + () => ctSave.Alias, + () => tryCreateTemplate.Result.StatusType); + } + template = tryCreateTemplate.Result.Entity; + } + + //make sure the template alias is set on the default and allowed template so we can map it back + ctSave.DefaultTemplate = template.Alias; + + } + }); + + var display = Mapper.Map(savedCt); + + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + string.Empty); + + return display; + } + + /// + /// Returns an empty content type for use as a scaffold when creating a new type + /// + /// + /// + public DocumentTypeDisplay GetEmpty(int parentId) + { + IContentType ct; + if (parentId != Constants.System.Root) + { + var parent = Services.ContentTypeService.GetContentType(parentId); + ct = parent != null ? new ContentType(parent, string.Empty) : new ContentType(parentId); + } + else + ct = new ContentType(parentId); + + ct.Icon = "icon-document"; + + var dto = Mapper.Map(ct); + return dto; + } + + + /// + /// Returns all content type objects + /// + public IEnumerable GetAll() + { + var types = Services.ContentTypeService.GetAllContentTypes(); + var basics = types.Select(Mapper.Map); + + return basics.Select(basic => + { + basic.Name = TranslateItem(basic.Name); + basic.Description = TranslateItem(basic.Description); + return basic; + }); + } + /// /// Returns the allowed child content type objects for the content item id passed in /// /// + [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes, Constants.Trees.Content)] public IEnumerable GetAllowedChildren(int contentId) { if (contentId == Constants.System.RecycleBinContent) @@ -71,7 +272,7 @@ namespace Umbraco.Web.Editors types = Services.ContentTypeService.GetAllContentTypes().ToList(); //if no allowed root types are set, just return everything - if(types.Any(x => x.AllowedAsRoot)) + if (types.Any(x => x.AllowedAsRoot)) types = types.Where(x => x.AllowedAsRoot); } else @@ -79,11 +280,11 @@ namespace Umbraco.Web.Editors var contentItem = Services.ContentService.GetById(contentId); if (contentItem == null) { - throw new HttpResponseException(HttpStatusCode.NotFound); + return Enumerable.Empty(); } var ids = contentItem.ContentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); - + if (ids.Any() == false) return Enumerable.Empty(); types = Services.ContentTypeService.GetAllContentTypes(ids).ToList(); @@ -91,38 +292,40 @@ namespace Umbraco.Web.Editors var basics = types.Select(Mapper.Map).ToList(); + var localizedTextService = Services.TextService; foreach (var basic in basics) { - basic.Name = TranslateItem(basic.Name); - basic.Description = TranslateItem(basic.Description); + basic.Name = localizedTextService.UmbracoDictionaryTranslate(basic.Name); + basic.Description = localizedTextService.UmbracoDictionaryTranslate(basic.Description); } return basics; } - // TODO: This should really be centralized and used anywhere globalization applies. - internal string TranslateItem(string text) + /// + /// Move the content type + /// + /// + /// + public HttpResponseMessage PostMove(MoveOrCopy move) { - if (text == null) - { - return null; - } - - if (text.StartsWith("#") == false) - return text; - - text = text.Substring(1); - return CultureDictionary[text].IfNullOrWhiteSpace(text); + return PerformMove( + move, + getContentType: i => Services.ContentTypeService.GetContentType(i), + doMove: (type, i) => Services.ContentTypeService.MoveContentType(type, i)); } - private ICultureDictionary CultureDictionary + /// + /// Copy the content type + /// + /// + /// + public HttpResponseMessage PostCopy(MoveOrCopy copy) { - get - { - return - _cultureDictionary ?? - (_cultureDictionary = CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary()); - } + return PerformCopy( + copy, + getContentType: i => Services.ContentTypeService.GetContentType(i), + doCopy: (type, i) => Services.ContentTypeService.CopyContentType(type, i)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 0c3b61d9eb..0e699b591a 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -1,73 +1,482 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using System.Web.Http; -using AutoMapper; -using Newtonsoft.Json; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Editors -{ - /// - /// Am abstract API controller providing functionality used for dealing with content and media types - /// - [PluginController("UmbracoApi")] - public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController - { - /// - /// Constructor - /// - protected ContentTypeControllerBase() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - protected ContentTypeControllerBase(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - public DataTypeBasic GetAssignedListViewDataType(int contentTypeId) - { - var objectType = Services.EntityService.GetObjectType(contentTypeId); - - switch (objectType) - { - case UmbracoObjectTypes.MemberType: - var memberType = Services.MemberTypeService.Get(contentTypeId); - var dtMember = Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + memberType.Alias); - return dtMember == null - ? Mapper.Map( - Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + "Member")) - : Mapper.Map(dtMember); - case UmbracoObjectTypes.MediaType: - var mediaType = Services.ContentTypeService.GetMediaType(contentTypeId); - var dtMedia = Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + mediaType.Alias); - return dtMedia == null - ? Mapper.Map( - Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + "Media")) - : Mapper.Map(dtMedia); - case UmbracoObjectTypes.DocumentType: - var docType = Services.ContentTypeService.GetContentType(contentTypeId); - var dtDoc = Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + docType.Alias); - return dtDoc == null - ? Mapper.Map( - Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + "Content")) - : Mapper.Map(dtDoc); - default: - throw new ArgumentOutOfRangeException(); - } - } - - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.Http; +using AutoMapper; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Exceptions; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + /// + /// Am abstract API controller providing functionality used for dealing with content and media types + /// + [PluginController("UmbracoApi")] + [PrefixlessBodyModelValidator] + public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController + { + private ICultureDictionary _cultureDictionary; + + /// + /// Constructor + /// + protected ContentTypeControllerBase() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + protected ContentTypeControllerBase(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Returns the available composite content types for a given content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + /// + protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, + UmbracoObjectTypes type, + string[] filterContentTypes, + string[] filterPropertyTypes) + { + IContentTypeComposition source = null; + + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic + + IContentTypeComposition[] allContentTypes; + + switch (type) + { + case UmbracoObjectTypes.DocumentType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetContentType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); + break; + + case UmbracoObjectTypes.MediaType: + if (contentTypeId > 0) + { + source = Services.ContentTypeService.GetMediaType(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); + break; + + case UmbracoObjectTypes.MemberType: + if (contentTypeId > 0) + { + source = Services.MemberTypeService.Get(contentTypeId); + if (source == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + allContentTypes = Services.MemberTypeService.GetAll().Cast().ToArray(); + break; + + default: + throw new ArgumentOutOfRangeException("The entity type was not a content type"); + } + + var availableCompositions = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); + + var currCompositions = source == null ? new IContentTypeComposition[] { } : source.ContentTypeComposition.ToArray(); + var compAliases = currCompositions.Select(x => x.Alias).ToArray(); + var ancestors = availableCompositions.Ancestors.Select(x => x.Alias); + + return availableCompositions.Results + .Select(x => new Tuple(Mapper.Map(x.Composition), x.Allowed)) + .Select(x => + { + //translate the name + x.Item1.Name = TranslateItem(x.Item1.Name); + + //we need to ensure that the item is enabled if it is already selected + // but do not allow it if it is any of the ancestors + if (compAliases.Contains(x.Item1.Alias) && ancestors.Contains(x.Item1.Alias) == false) + { + //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) + x = new Tuple(x.Item1, true); + } + + return x; + }) + .ToList(); + } + + + protected string TranslateItem(string text) + { + if (text == null) + { + return null; + } + + if (text.StartsWith("#") == false) + return text; + + text = text.Substring(1); + return CultureDictionary[text].IfNullOrWhiteSpace(text); + } + + protected TContentType PerformPostSave( + TContentTypeSave contentTypeSave, + Func getContentType, + Action saveContentType, + Action beforeCreateNew = null) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + var ctId = Convert.ToInt32(contentTypeSave.Id); + var ct = ctId > 0 ? getContentType(ctId) : null; + if (ctId > 0 && ct == null) throw new HttpResponseException(HttpStatusCode.NotFound); + + //Validate that there's no other ct with the same alias + // it in fact cannot be the same as any content type alias (member, content or media) because + // this would interfere with how ModelsBuilder works and also how many of the published caches + // works since that is based on aliases. + var allAliases = Services.ContentTypeService.GetAllContentTypeAliases(); + var exists = allAliases.InvariantContains(contentTypeSave.Alias); + if ((exists) && (ctId == 0 || ct.Alias != contentTypeSave.Alias)) + { + ModelState.AddModelError("Alias", "A content type, media type or member type with this alias already exists"); + } + + //now let the external validators execute + ValidationHelper.ValidateEditorModelWithResolver(ModelState, contentTypeSave); + + if (ModelState.IsValid == false) + { + throw CreateModelStateValidationException(ctId, contentTypeSave, ct); + } + + //filter out empty properties + contentTypeSave.Groups = contentTypeSave.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); + foreach (var group in contentTypeSave.Groups) + { + group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); + } + + if (ctId > 0) + { + //its an update to an existing content type + + //This mapping will cause a lot of content type validation to occur which we need to deal with + try + { + Mapper.Map(contentTypeSave, ct); + } + catch (Exception ex) + { + var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); + if (responseEx != null) throw responseEx; + } + + var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, ct); + if (exResult != null) throw exResult; + + saveContentType(ct); + + return ct; + } + else + { + if (beforeCreateNew != null) + { + beforeCreateNew(contentTypeSave); + } + + //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type + //always filter these 0 types out + var allowItselfAsChild = false; + if (contentTypeSave.AllowedContentTypes != null) + { + allowItselfAsChild = contentTypeSave.AllowedContentTypes.Any(x => x == 0); + contentTypeSave.AllowedContentTypes = contentTypeSave.AllowedContentTypes.Where(x => x > 0).ToList(); + } + + //save as new + + TContentType newCt = null; + try + { + //This mapping will cause a lot of content type validation to occur which we need to deal with + newCt = Mapper.Map(contentTypeSave); + } + catch (Exception ex) + { + var responseEx = CreateInvalidCompositionResponseException(ex, contentTypeSave, ct, ctId); + if (responseEx != null) throw responseEx; + } + + var exResult = CreateCompositionValidationExceptionIfInvalid(contentTypeSave, newCt); + if (exResult != null) throw exResult; + + //set id to null to ensure its handled as a new type + contentTypeSave.Id = null; + contentTypeSave.CreateDate = DateTime.Now; + contentTypeSave.UpdateDate = DateTime.Now; + + saveContentType(newCt); + + //we need to save it twice to allow itself under itself. + if (allowItselfAsChild) + { + //NOTE: This will throw if the composition isn't right... but it shouldn't be at this stage + newCt.AddContentType(newCt); + saveContentType(newCt); + } + return newCt; + } + } + + /// + /// Move + /// + /// + /// + /// + /// + protected HttpResponseMessage PerformMove( + MoveOrCopy move, + Func getContentType, + Func>> doMove) + where TContentType : IContentTypeComposition + { + var toMove = getContentType(move.Id); + if (toMove == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var result = doMove(toMove, move.ParentId); + if (result.Success) + { + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); + case MoveOperationStatusType.FailedCancelledByEvent: + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + return Request.CreateValidationErrorResponse(notificationModel); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Move + /// + /// + /// + /// + /// + protected HttpResponseMessage PerformCopy( + MoveOrCopy move, + Func getContentType, + Func>> doCopy) + where TContentType : IContentTypeComposition + { + var toMove = getContentType(move.Id); + if (toMove == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var result = doCopy(toMove, move.ParentId); + if (result.Success) + { + var copy = result.Result.Entity; + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(copy.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); + case MoveOperationStatusType.FailedCancelledByEvent: + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + return Request.CreateValidationErrorResponse(notificationModel); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors + /// + /// + /// + /// + private HttpResponseException CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, IContentTypeComposition composition) + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + where TContentTypeDisplay : ContentTypeCompositionDisplay + { + var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + if (validateAttempt == false) + { + //if it's not successful then we need to return some model state for the property aliases that + // are duplicated + var invalidPropertyAliases = validateAttempt.Result.Distinct(); + AddCompositionValidationErrors(contentTypeSave, invalidPropertyAliases); + + var display = Mapper.Map(composition); + //map the 'save' data on top + display = Mapper.Map(contentTypeSave, display); + display.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + return null; + } + + /// + /// Adds errors to the model state if any invalid aliases are found then throws an error response if there are errors + /// + /// + /// + /// + private void AddCompositionValidationErrors(TContentTypeSave contentTypeSave, IEnumerable invalidPropertyAliases) + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + foreach (var propertyAlias in invalidPropertyAliases) + { + //find the property relating to these + var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias); + var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop)); + + var key = string.Format("Groups[{0}].Properties[{1}].Alias", group.SortOrder, prop.SortOrder); + ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions"); + } + } + + /// + /// If the exception is an InvalidCompositionException create a response exception to be thrown for validation errors + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private HttpResponseException CreateInvalidCompositionResponseException( + Exception ex, TContentTypeSave contentTypeSave, TContentType ct, int ctId) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + InvalidCompositionException invalidCompositionException = null; + if (ex is AutoMapperMappingException && ex.InnerException is InvalidCompositionException) + { + invalidCompositionException = (InvalidCompositionException)ex.InnerException; + } + else if (ex.InnerException is InvalidCompositionException) + { + invalidCompositionException = (InvalidCompositionException)ex; + } + if (invalidCompositionException != null) + { + AddCompositionValidationErrors(contentTypeSave, invalidCompositionException.PropertyTypeAliases); + return CreateModelStateValidationException(ctId, contentTypeSave, ct); + } + return null; + } + + /// + /// Used to throw the ModelState validation results when the ModelState is invalid + /// + /// + /// + /// + /// + /// + /// + private HttpResponseException CreateModelStateValidationException(int ctId, TContentTypeSave contentTypeSave, TContentType ct) + where TContentType : class, IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave + { + TContentTypeDisplay forDisplay; + if (ctId > 0) + { + //Required data is invalid so we cannot continue + forDisplay = Mapper.Map(ct); + //map the 'save' data on top + forDisplay = Mapper.Map(contentTypeSave, forDisplay); + } + else + { + //map the 'save' data to display + forDisplay = Mapper.Map(contentTypeSave); + } + + forDisplay.Errors = ModelState.ToErrorDictionary(); + return new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } + + private ICultureDictionary CultureDictionary + { + get + { + return + _cultureDictionary ?? + (_cultureDictionary = CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary()); + } + } + + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 0508e43fc1..931d00968b 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -19,6 +19,7 @@ using Umbraco.Web.WebApi.Filters; using umbraco; using Constants = Umbraco.Core.Constants; using System.Net.Http; +using System.Text; namespace Umbraco.Web.Editors { @@ -26,14 +27,19 @@ namespace Umbraco.Web.Editors /// The API controller used for editing data types /// /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the developer application. + /// The security for this controller is defined to allow full CRUD access to data types if the user has access to either: + /// Content Types, Member Types or Media Types ... and of course to Data Types /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.DataTypes)] + [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.DocumentTypes, Constants.Trees.MediaTypes, Constants.Trees.MemberTypes)] [EnableOverrideAuthorization] public class DataTypeController : UmbracoAuthorizedJsonController { + /// + /// Gets data type by name + /// + /// + /// public DataTypeDisplay GetByName(string name) { var dataType = Services.DataTypeService.GetDataTypeDefinitionByName(name); @@ -41,14 +47,10 @@ namespace Umbraco.Web.Editors } /// - /// Gets the content json for the content id + /// Gets the datatype json for the datatype id /// /// /// - /// - /// Permission is granted to this method if the user has access to any of these trees: DataTypes, Content or Media - /// - [UmbracoTreeAuthorize(Constants.Trees.DataTypes, Constants.Trees.Content, Constants.Trees.Media)] public DataTypeDisplay GetById(int id) { var dataType = Services.DataTypeService.GetDataTypeDefinitionById(id); @@ -74,17 +76,53 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - Services.DataTypeService.Delete(foundType, UmbracoUser.Id); + Services.DataTypeService.Delete(foundType, Security.CurrentUser.Id); return Request.CreateResponse(HttpStatusCode.OK); } - public DataTypeDisplay GetEmpty() + public DataTypeDisplay GetEmpty(int parentId) { - var dt = new DataTypeDefinition(""); + var dt = new DataTypeDefinition(parentId, ""); return Mapper.Map(dt); } + /// + /// Returns a custom listview, based on a content type alias, if found + /// + /// + /// a DataTypeDisplay + public DataTypeDisplay GetCustomListView(string contentTypeAlias) + { + var dt = Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias); + if (dt == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return Mapper.Map(dt); + } + + /// + /// Creates a custom list view - give a document type alias + /// + /// + /// + public DataTypeDisplay PostCreateCustomListView(string contentTypeAlias) + { + var dt = Services.DataTypeService.GetDataTypeDefinitionByName(Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias); + + //if it doesnt exist yet, we will create it. + if (dt == null) + { + dt = new DataTypeDefinition( Constants.PropertyEditors.ListViewAlias ); + dt.Name = Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; + Services.DataTypeService.Save(dt); + } + + return Mapper.Map(dt); + } + /// /// Returns the pre-values for the specified property editor /// @@ -125,7 +163,28 @@ namespace Umbraco.Web.Editors return Mapper.Map>(propEd); } - //TODO: Generally there probably won't be file uploads for pre-values but we should allow them just like we do for the content editor + /// + /// Deletes a data type container wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteContainer(int id) + { + Services.DataTypeService.DeleteContainer(id, Security.CurrentUser.Id); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + public HttpResponseMessage PostCreateContainer(int parentId, string name) + { + var result = Services.DataTypeService.CreateContainer(parentId, name, Security.CurrentUser.Id); + + return result + ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id + : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + } /// /// Saves the data type @@ -169,5 +228,143 @@ namespace Umbraco.Web.Editors //now return the updated model return display; } + + /// + /// Move the media type + /// + /// + /// + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = Services.DataTypeService.GetDataTypeDefinitionById(move.Id); + if (toMove == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var result = Services.DataTypeService.Move(toMove, move.ParentId); + if (result.Success) + { + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); + case MoveOperationStatusType.FailedCancelledByEvent: + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + return Request.CreateValidationErrorResponse(notificationModel); + default: + throw new ArgumentOutOfRangeException(); + } + } + + #region ReadOnly actions to return basic data - allow access for: content ,media, members, settings, developer + /// + /// Gets the content json for all data types + /// + /// + /// + /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members + /// + [UmbracoApplicationAuthorize( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Developer)] + public IEnumerable GetAll() + { + return Services.DataTypeService + .GetAllDataTypeDefinitions() + .Select(Mapper.Map).Where(x => x.IsSystemDataType == false); + } + + /// + /// Returns all data types grouped by their property editor group + /// + /// + /// + /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members + /// + [UmbracoTreeAuthorize( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Developer)] + public IDictionary> GetGroupedDataTypes() + { + var dataTypes = Services.DataTypeService + .GetAllDataTypeDefinitions() + .Select(Mapper.Map) + .ToArray(); + + var propertyEditors = PropertyEditorResolver.Current.PropertyEditors.ToArray(); + + foreach (var dataType in dataTypes) + { + var propertyEditor = propertyEditors.SingleOrDefault(x => x.Alias == dataType.Alias); + if(propertyEditor != null) + dataType.HasPrevalues = propertyEditor.PreValueEditor.Fields.Any(); ; + } + + var grouped = dataTypes + .GroupBy(x => x.Group.IsNullOrWhiteSpace() ? "" : x.Group.ToLower()) + .ToDictionary(group => group.Key, group => group.OrderBy(d => d.Name).AsEnumerable()); + + return grouped; + } + + /// + /// Returns all property editors grouped + /// + /// + /// + /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members + /// + [UmbracoTreeAuthorize( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Developer)] + public IDictionary> GetGroupedPropertyEditors() + { + var datatypes = new List(); + + var propertyEditors = PropertyEditorResolver.Current.PropertyEditors; + foreach (var propertyEditor in propertyEditors) + { + var hasPrevalues = propertyEditor.PreValueEditor.Fields.Any(); + var basic = Mapper.Map(propertyEditor); + basic.HasPrevalues = hasPrevalues; + datatypes.Add(basic); + } + + var grouped = datatypes + .GroupBy(x => x.Group.IsNullOrWhiteSpace() ? "" : x.Group.ToLower()) + .ToDictionary(group => group.Key, group => group.OrderBy(d => d.Name).AsEnumerable()); + + return grouped; + } + + + /// + /// Gets all property editors defined + /// + /// + /// + /// Permission is granted to this method if the user has access to any of these sections: Content, media, settings, developer, members + /// + [UmbracoTreeAuthorize( + Constants.Applications.Content, Constants.Applications.Media, Constants.Applications.Members, + Constants.Applications.Settings, Constants.Applications.Developer)] + public IEnumerable GetAllPropertyEditors() + { + return PropertyEditorResolver.Current.PropertyEditors + .OrderBy(x => x.Name) + .Select(Mapper.Map); + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs index 6757082a49..7f513b35c7 100644 --- a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs +++ b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs @@ -42,6 +42,9 @@ namespace Umbraco.Web.Editors { var dataType = (DataTypeSave)actionContext.ActionArguments["dataType"]; + dataType.Name = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); + dataType.Alias = dataType.Name.CleanForXss('[', ']', '(', ')', ':'); + //Validate that the property editor exists var propertyEditor = PropertyEditorResolver.Current.GetByAlias(dataType.SelectedEditor); if (propertyEditor == null) diff --git a/src/Umbraco.Web/Editors/EditorModelEventManager.cs b/src/Umbraco.Web/Editors/EditorModelEventManager.cs new file mode 100644 index 0000000000..44454ca6c3 --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorModelEventManager.cs @@ -0,0 +1,91 @@ +using System; +using System.Web.Http.Filters; +using Umbraco.Core.Events; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + public class EditorModelEventArgs : EventArgs + { + public EditorModelEventArgs(object model, UmbracoContext umbracoContext) + { + Model = model; + UmbracoContext = umbracoContext; + } + + public object Model { get; private set; } + public UmbracoContext UmbracoContext { get; private set; } + } + + public sealed class EditorModelEventArgs : EditorModelEventArgs + { + public EditorModelEventArgs(EditorModelEventArgs baseArgs) + : base(baseArgs.Model, baseArgs.UmbracoContext) + { + Model = (T)baseArgs.Model; + } + + public EditorModelEventArgs(T model, UmbracoContext umbracoContext) + : base(model, umbracoContext) + { + Model = model; + } + + public new T Model { get; private set; } + } + + /// + /// Used to emit events for editor models in the back office + /// + public sealed class EditorModelEventManager + { + public static event TypedEventHandler> SendingContentModel; + public static event TypedEventHandler> SendingMediaModel; + public static event TypedEventHandler> SendingMemberModel; + + private static void OnSendingContentModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingContentModel; + if (handler != null) handler(sender, e); + } + + private static void OnSendingMediaModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingMediaModel; + if (handler != null) handler(sender, e); + } + + private static void OnSendingMemberModel(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var handler = SendingMemberModel; + if (handler != null) handler(sender, e); + } + + /// + /// Based on the type, emit's a specific event + /// + /// + /// + internal static void EmitEvent(HttpActionExecutedContext sender, EditorModelEventArgs e) + { + var contentItemDisplay = e.Model as ContentItemDisplay; + if (contentItemDisplay != null) + { + OnSendingContentModel(sender, new EditorModelEventArgs(e)); + } + + var mediaItemDisplay = e.Model as MediaItemDisplay; + if (mediaItemDisplay != null) + { + OnSendingMediaModel(sender, new EditorModelEventArgs(e)); + } + + var memberItemDisplay = e.Model as MemberDisplay; + if (memberItemDisplay != null) + { + OnSendingMemberModel(sender, new EditorModelEventArgs(e)); + } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EditorValidationResolver.cs b/src/Umbraco.Web/Editors/EditorValidationResolver.cs new file mode 100644 index 0000000000..d0c5edf5af --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorValidationResolver.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.Editors +{ + internal class EditorValidationResolver : LazyManyObjectsResolverBase + { + public EditorValidationResolver(IServiceProvider serviceProvider, ILogger logger, Func> migrations) + : base(serviceProvider, logger, migrations, ObjectLifetimeScope.Application) + { + } + + public virtual IEnumerable EditorValidators + { + get { return Values; } + } + + public IEnumerable Validate(object model) + { + return EditorValidators + .Where(x => model.GetType() == x.ModelType) + .WhereNotNull() + .SelectMany(x => x.Validate(model)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EditorValidator.cs b/src/Umbraco.Web/Editors/EditorValidator.cs new file mode 100644 index 0000000000..d123838f52 --- /dev/null +++ b/src/Umbraco.Web/Editors/EditorValidator.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.Editors +{ + internal abstract class EditorValidator : IEditorValidator + { + public Type ModelType + { + get { return typeof (T); } + } + + protected abstract IEnumerable PerformValidate(T model); + + public IEnumerable Validate(object model) + { + return PerformValidate((T) model); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 90b15a73c7..596e27e3a5 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -42,6 +42,23 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class EntityController : UmbracoAuthorizedJsonController { + /// + /// Returns an Umbraco alias given a string + /// + /// + /// + /// + public dynamic GetSafeAlias(string value, bool camelCase = true) + { + var returnValue = (string.IsNullOrWhiteSpace(value)) ? string.Empty : value.ToSafeAlias(camelCase); + dynamic returnObj = new System.Dynamic.ExpandoObject(); + returnObj.alias = returnValue; + returnObj.original = value; + returnObj.camelCase = camelCase; + + return returnObj; + } + /// /// Searches for results based on the entity type /// @@ -535,6 +552,11 @@ namespace Umbraco.Web.Editors //now we need to convert the unknown ones switch (entityType) { + case UmbracoEntityTypes.Template: + var templates = Services.FileService.GetTemplates(); + var filteredTemplates = ExecutePostFilter(templates, postFilter, postFilterParams); + return filteredTemplates.Select(Mapper.Map); + case UmbracoEntityTypes.Macro: //Get all macros from the macro service var macros = Services.MacroService.GetAll().WhereNotNull().OrderBy(x => x.Name); @@ -717,8 +739,6 @@ namespace Umbraco.Web.Editors return UmbracoObjectTypes.Media; case UmbracoEntityTypes.MemberType: return UmbracoObjectTypes.MediaType; - case UmbracoEntityTypes.Template: - return UmbracoObjectTypes.Template; case UmbracoEntityTypes.MemberGroup: return UmbracoObjectTypes.MemberGroup; case UmbracoEntityTypes.ContentItem: diff --git a/src/Umbraco.Web/Editors/GravatarController.cs b/src/Umbraco.Web/Editors/GravatarController.cs new file mode 100644 index 0000000000..f1e184dce7 --- /dev/null +++ b/src/Umbraco.Web/Editors/GravatarController.cs @@ -0,0 +1,38 @@ +using System; +using System.Net; +using Umbraco.Core; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + /// + /// API controller used for getting Gravatar urls + /// + [PluginController("UmbracoApi")] + public class GravatarController : UmbracoAuthorizedJsonController + { + public string GetCurrentUserGravatarUrl() + { + var userService = Services.UserService; + var user = userService.GetUserById(UmbracoContext.Security.CurrentUser.Id); + var gravatarHash = user.Email.ToMd5(); + var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash; + + // Test if we can reach this URL, will fail when there's network or firewall errors + var request = (HttpWebRequest)WebRequest.Create(gravatarUrl); + // Require response within 10 seconds + request.Timeout = 10000; + try + { + using ((HttpWebResponse)request.GetResponse()) { } + } + catch (Exception) + { + // There was an HTTP or other error, return an null instead + return null; + } + + return gravatarUrl; + } + } +} diff --git a/src/Umbraco.Web/Editors/IEditorValidator.cs b/src/Umbraco.Web/Editors/IEditorValidator.cs new file mode 100644 index 0000000000..e1d4e68ed2 --- /dev/null +++ b/src/Umbraco.Web/Editors/IEditorValidator.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.Editors +{ + internal interface IEditorValidator + { + Type ModelType { get; } + IEnumerable Validate(object model); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 54938be76d..416ffc2553 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -1,175 +1,132 @@ -using System; -using System.Drawing; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Media; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Editors -{ - /// - /// A controller used to return images for media - /// - [PluginController("UmbracoApi")] - public class ImagesController : UmbracoAuthorizedApiController - { - /// - /// Gets the big thumbnail image for the media id - /// - /// - /// - /// - /// If there is no media, image property or image file is found then this will return not found. - /// - public HttpResponseMessage GetBigThumbnail(int mediaId) - { - var media = Services.MediaService.GetById(mediaId); - if (media == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - var imageProp = media.Properties[Constants.Conventions.Media.File]; - if (imageProp == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - var imagePath = imageProp.Value.ToString(); - - return GetBigThumbnail(imagePath); - } - - /// - /// Gets the big thumbnail image for the original image path - /// - /// - /// - /// - /// If there is no original image is found then this will return not found. - /// - public HttpResponseMessage GetBigThumbnail(string originalImagePath) - { - if (string.IsNullOrWhiteSpace(originalImagePath)) - return Request.CreateResponse(HttpStatusCode.OK); - - return GetResized(originalImagePath, 500, "big-thumb"); - } - - /// - /// Gets a resized image for the media id - /// - /// - /// - /// - /// - /// If there is no media, image property or image file is found then this will return not found. - /// - public HttpResponseMessage GetResized(int mediaId, int width) - { - var media = Services.MediaService.GetById(mediaId); - if (media == null) - { - return new HttpResponseMessage(HttpStatusCode.NotFound); - } - var imageProp = media.Properties[Constants.Conventions.Media.File]; - if (imageProp == null) - { - return new HttpResponseMessage(HttpStatusCode.NotFound); - } - - var imagePath = imageProp.Value.ToString(); - - return GetResized(imagePath, width); - } - - /// - /// Gets a resized image for the image at the given path - /// - /// - /// - /// - /// - /// If there is no media, image property or image file is found then this will return not found. - /// - public HttpResponseMessage GetResized(string imagePath, int width) - { - return GetResized(imagePath, width, Convert.ToString(width)); - } - - //TODO: We should delegate this to ImageProcessing - - /// - /// Gets a resized image - if the requested max width is greater than the original image, only the original image will be returned. - /// - /// - /// - /// - /// - private HttpResponseMessage GetResized(string imagePath, int width, string suffix) - { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); - var ext = Path.GetExtension(imagePath); - - //we need to check if it is an image by extension - if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(ext.TrimStart('.')) == false) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - var thumbFilePath = imagePath.TrimEnd(ext) + "_" + suffix + ".jpg"; - var fullOrgPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(imagePath)); - var fullNewPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(thumbFilePath)); - var thumbIsNew = mediaFileSystem.FileExists(fullNewPath) == false; - if (thumbIsNew) - { - //we need to generate it - if (mediaFileSystem.FileExists(fullOrgPath) == false) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - using (var fileStream = mediaFileSystem.OpenFile(fullOrgPath)) - { - if (fileStream.CanSeek) fileStream.Seek(0, 0); - using (var originalImage = Image.FromStream(fileStream)) - { - //If it is bigger, then do the resize - if (originalImage.Width >= width && originalImage.Height >= width) - { - ImageHelper.GenerateThumbnail( - originalImage, - width, - fullNewPath, - "jpg", - mediaFileSystem); - } - else - { - //just return the original image - fullNewPath = fullOrgPath; - } - - } - } - } - - var result = Request.CreateResponse(HttpStatusCode.OK); - //NOTE: That we are not closing this stream as the framework will do that for us, if we try it will - // fail. See http://stackoverflow.com/questions/9541351/returning-binary-file-from-controller-in-asp-net-web-api - var stream = mediaFileSystem.OpenFile(fullNewPath); - if (stream.CanSeek) stream.Seek(0, 0); - result.Content = new StreamContent(stream); - result.Headers.Date = mediaFileSystem.GetLastModified(imagePath); - result.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg"); - return result; - } - } +using System; +using System.Drawing; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Media; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + /// + /// A controller used to return images for media + /// + [PluginController("UmbracoApi")] + public class ImagesController : UmbracoAuthorizedApiController + { + /// + /// Gets the big thumbnail image for the media id + /// + /// + /// + /// + /// If there is no media, image property or image file is found then this will return not found. + /// + public HttpResponseMessage GetBigThumbnail(int mediaId) + { + var media = Services.MediaService.GetById(mediaId); + if (media == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + var imageProp = media.Properties[Constants.Conventions.Media.File]; + if (imageProp == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var imagePath = imageProp.Value.ToString(); + + return GetBigThumbnail(imagePath); + } + + /// + /// Gets the big thumbnail image for the original image path + /// + /// + /// + /// + /// If there is no original image is found then this will return not found. + /// + public HttpResponseMessage GetBigThumbnail(string originalImagePath) + { + if (string.IsNullOrWhiteSpace(originalImagePath)) + return Request.CreateResponse(HttpStatusCode.OK); + + return GetResized(originalImagePath, 500, "big-thumb"); + } + + /// + /// Gets a resized image for the media id + /// + /// + /// + /// + /// + /// If there is no media, image property or image file is found then this will return not found. + /// + public HttpResponseMessage GetResized(int mediaId, int width) + { + var media = Services.MediaService.GetById(mediaId); + if (media == null) + { + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + var imageProp = media.Properties[Constants.Conventions.Media.File]; + if (imageProp == null) + { + return new HttpResponseMessage(HttpStatusCode.NotFound); + } + + var imagePath = imageProp.Value.ToString(); + + return GetResized(imagePath, width); + } + + /// + /// Gets a resized image for the image at the given path + /// + /// + /// + /// + /// + /// If there is no media, image property or image file is found then this will return not found. + /// + public HttpResponseMessage GetResized(string imagePath, int width) + { + return GetResized(imagePath, width, Convert.ToString(width)); + } + + /// + /// Gets a resized image - if the requested max width is greater than the original image, only the original image will be returned. + /// + /// + /// + /// + /// + private HttpResponseMessage GetResized(string imagePath, int width, string suffix) + { + var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); + var ext = Path.GetExtension(imagePath); + + //we need to check if it is an image by extension + if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(ext.TrimStart('.')) == false) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file + var response = Request.CreateResponse( HttpStatusCode.Found ); + var imageLastModified = mediaFileSystem.GetLastModified( imagePath ); + response.Headers.Location = new Uri( string.Format( "{0}?rnd={1}&width={2}", imagePath, string.Format( "{0:yyyyMMddHHmmss}", imageLastModified ), width ), UriKind.Relative ); + return response; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/LegacyController.cs b/src/Umbraco.Web/Editors/LegacyController.cs index 8def2cc7e8..59218771e8 100644 --- a/src/Umbraco.Web/Editors/LegacyController.cs +++ b/src/Umbraco.Web/Editors/LegacyController.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Editors if (httpContextAttempt.Success) { //this is a hack check based on legacy - if (nodeType == "memberGroup") + if (nodeType == "memberGroups") { LegacyDialogHandler.Delete(httpContextAttempt.Result, UmbracoUser, nodeType, 0, alias); return Request.CreateResponse(HttpStatusCode.OK); diff --git a/src/Umbraco.Web/Editors/MacroController.cs b/src/Umbraco.Web/Editors/MacroController.cs index 11fef4a25d..aed6cec602 100644 --- a/src/Umbraco.Web/Editors/MacroController.cs +++ b/src/Umbraco.Web/Editors/MacroController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -34,7 +35,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - return Mapper.Map>(macro); + return Mapper.Map>(macro).OrderBy(x => x.SortOrder); } /// @@ -49,7 +50,33 @@ namespace Umbraco.Web.Editors /// /// /// - public HttpResponseMessage GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromUri]IDictionary macroParams) + [HttpGet] + public HttpResponseMessage GetMacroResultAsHtmlForEditor(string macroAlias, int pageId, [FromUri] IDictionary macroParams) + { + return GetMacroResultAsHtml(macroAlias, pageId, macroParams); + } + + /// + /// Gets a rendered macro as html for rendering in the rich text editor. + /// Using HTTP POST instead of GET allows for more parameters to be passed as it's not dependant on URL-length limitations like GET. + /// The method using GET is kept to maintain backwards compatibility + /// + /// + /// + [HttpPost] + public HttpResponseMessage GetMacroResultAsHtmlForEditor(MacroParameterModel model) + { + return GetMacroResultAsHtml(model.MacroAlias, model.PageId, model.MacroParams); + } + + public class MacroParameterModel + { + public string MacroAlias { get; set; } + public int PageId { get; set; } + public IDictionary MacroParams { get; set; } + } + + private HttpResponseMessage GetMacroResultAsHtml(string macroAlias, int pageId, IDictionary macroParams) { // note - here we should be using the cache, provided that the preview content is in the cache... @@ -81,7 +108,7 @@ namespace Umbraco.Web.Editors //the 'easiest' way might be to create an IPublishedContent manually and populate the legacy 'page' object with that //and then set the legacy parameters. - var legacyPage = new global::umbraco.page(doc); + var legacyPage = new global::umbraco.page(doc); UmbracoContext.HttpContext.Items["pageID"] = doc.Id; UmbracoContext.HttpContext.Items["pageElements"] = legacyPage.Elements; UmbracoContext.HttpContext.Items[global::Umbraco.Core.Constants.Conventions.Url.AltTemplate] = null; diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e03f55a785..d9941324be 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Editors /// public MediaController() : this(UmbracoContext.Current) - { + { } /// @@ -63,13 +63,14 @@ namespace Umbraco.Web.Editors : base(umbracoContext) { } - + /// /// Gets an empty content item for the /// /// /// /// + [OutgoingEditorModelEvent] public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) { var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); @@ -87,11 +88,35 @@ namespace Umbraco.Web.Editors return mapped; } + /// + /// Returns an item to be used to display the recycle bin for media + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinMedia, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinMedia + }; + + TabsAndPropertiesResolver.AddListView(display, "media", Services.DataTypeService, Services.TextService); + + return display; + } + /// /// Gets the content json for the content id /// /// /// + [OutgoingEditorModelEvent] [EnsureUserPermissionForMedia("id")] public MediaItemDisplay GetById(int id) { @@ -118,6 +143,21 @@ namespace Umbraco.Web.Editors return foundMedia.Select(Mapper.Map); } + /// + /// Returns media items known to be a container of other media items + /// + /// + /// + [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] + public IEnumerable> GetChildFolders(int id = -1) + { + //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... + //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" + var folderTypes = Services.ContentTypeService.GetAllMediaTypes().ToArray().Where(x => x.Alias.EndsWith("Folder")).Select(x => x.Id); + + var children = (id < 0) ? Services.MediaService.GetRootMedia() : Services.MediaService.GetById(id).Children(); + return children.Where(x => folderTypes.Contains(x.ContentTypeId)).Select(Mapper.Map>); + } /// /// Returns the root media objects @@ -140,13 +180,16 @@ namespace Umbraco.Web.Editors int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "") { - int totalChildren; + long totalChildren; IMedia[] children; if (pageNumber > 0 && pageSize > 0) { - children = Services.MediaService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); + children = Services.MediaService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); } else { @@ -221,7 +264,7 @@ namespace Umbraco.Web.Editors var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; + return response; } /// @@ -267,7 +310,7 @@ namespace Umbraco.Web.Editors //return the updated model var display = Mapper.Map(contentItem.PersistedContent); - + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -294,8 +337,8 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); } } - - break; + + break; } return display; @@ -324,7 +367,7 @@ namespace Umbraco.Web.Editors Services.MediaService.EmptyRecycleBin(); return Request.CreateResponse(HttpStatusCode.OK); } - + /// /// Change the sort order for media /// @@ -343,7 +386,7 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.OK); } - + var mediaService = base.ApplicationContext.Services.MediaService; var sortedMedia = new List(); try @@ -365,7 +408,7 @@ namespace Umbraco.Web.Editors } } - [EnsureUserPermissionForMedia("folder.ParentId")] + [EnsureUserPermissionForMedia("folder.ParentId")] public MediaItemDisplay PostAddFolder(EntityBasic folder) { var mediaService = ApplicationContext.Services.MediaService; @@ -396,7 +439,7 @@ namespace Umbraco.Web.Editors var provider = new MultipartFormDataStreamProvider(root); var result = await Request.Content.ReadAsMultipartAsync(provider); - + //must have a file if (result.FileData.Count == 0) { @@ -412,7 +455,7 @@ namespace Umbraco.Web.Editors //ensure the user has access to this folder by parent id! if (CheckPermissions( - new Dictionary(), + new Dictionary(), Security.CurrentUser, Services.MediaService, parentId) == false) { @@ -425,12 +468,63 @@ namespace Umbraco.Web.Editors } var tempFiles = new PostedFiles(); + var mediaService = ApplicationContext.Services.MediaService; + + + //in case we pass a path with a folder in it, we will create it and upload media to it. + if (result.FormData.ContainsKey("path")) + { + + var folders = result.FormData["path"].Split('/'); + + for (int i = 0; i < folders.Length - 1; i++) + { + var folderName = folders[i]; + IMedia folderMediaItem; + + //if uploading directly to media root and not a subfolder + if (parentId == -1) + { + //look for matching folder + folderMediaItem = + mediaService.GetRootMedia().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, -1, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + else + { + //get current parent + var mediaRoot = mediaService.GetById(parentId); + + //if the media root is null, something went wrong, we'll abort + if (mediaRoot == null) + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, + "The folder: " + folderName + " could not be used for storing images, its ID: " + parentId + + " returned null"); + + //look for matching folder + folderMediaItem = mediaRoot.Children().FirstOrDefault(x => x.Name == folderName && x.ContentType.Alias == Constants.Conventions.MediaTypes.Folder); + if (folderMediaItem == null) + { + //if null, create a folder + folderMediaItem = mediaService.CreateMedia(folderName, mediaRoot, Constants.Conventions.MediaTypes.Folder); + mediaService.Save(folderMediaItem); + } + } + //set the media root to the folder id so uploaded files will end there. + parentId = folderMediaItem.Id; + } + } //get the files foreach (var file in result.FileData) { var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); - var ext = fileName.Substring(fileName.LastIndexOf('.')+1).ToLower(); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) { @@ -439,8 +533,17 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) mediaType = Constants.Conventions.MediaTypes.Image; - var mediaService = ApplicationContext.Services.MediaService; - var f = mediaService.CreateMedia(fileName, parentId, mediaType, Security.CurrentUser.Id); + //TODO: make the media item name "nice" since file names could be pretty ugly, we have + // string extensions to do much of this but we'll need: + // * Pascalcase the name (use string extensions) + // * strip the file extension + // * underscores to spaces + // * probably remove 'ugly' characters - let's discuss + // All of this logic should exist in a string extensions method and be unit tested + // http://issues.umbraco.org/issue/U4-5572 + var mediaItemName = fileName; + + var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); var fileInfo = new FileInfo(file.LocalFileName); var fs = fileInfo.OpenReadWithRetry(); @@ -454,7 +557,7 @@ namespace Umbraco.Web.Editors if (saveResult == false) { AddCancelMessage(tempFiles, - message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + fileName, + message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName, localizeMessage: false); } else @@ -471,7 +574,7 @@ namespace Umbraco.Web.Editors { tempFiles.Notifications.Add(new Notification( Services.TextService.Localize("speechBubbles/operationFailedHeader"), - "Cannot upload file " + file + ", it is not an approved file type", + Services.TextService.Localize("media/disallowedFileType"), SpeechBubbleIcon.Warning)); } } @@ -483,7 +586,7 @@ namespace Umbraco.Web.Editors if (origin.Value == "blueimp") { return Request.CreateResponse(HttpStatusCode.OK, - tempFiles, + tempFiles, //Don't output the angular xsrf stuff, blue imp doesn't like that new JsonMediaTypeFormatter()); } @@ -533,8 +636,9 @@ namespace Umbraco.Web.Editors //cannot move if the content item is not allowed at the root if (toMove.ContentType.AllowedAsRoot == false) { - throw new HttpResponseException( - Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedAtRoot", Security.CurrentUser))); + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); } } else @@ -549,15 +653,17 @@ namespace Umbraco.Web.Editors if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { - throw new HttpResponseException( - Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedByContentType", Security.CurrentUser))); + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); } // Check on paths if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { - throw new HttpResponseException( - Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedByPath", Security.CurrentUser))); + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); } } diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index e8cad40803..31be4509fb 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -1,26 +1,37 @@ using System.Collections.Generic; using System.Linq; -using System.Net; -using System.Web.Http; +using System.Web.Security; using AutoMapper; -using Newtonsoft.Json; +using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; +using System.Web.Http; +using System.Net; +using Umbraco.Core.PropertyEditors; +using System; +using System.Net.Http; +using System.Text; using Umbraco.Web.WebApi; +using ContentType = System.Net.Mime.ContentType; +using Umbraco.Core.Services; +using Umbraco.Web.Models; namespace Umbraco.Web.Editors { - - //TODO: We'll need to be careful about the security on this controller, when we start implementing + //TODO: We'll need to be careful about the security on this controller, when we start implementing // methods to modify content types we'll need to enforce security on the individual methods, we // cannot put security on the whole controller because things like GetAllowedChildren are required for content editing. /// - /// An API controller used for dealing with media types + /// An API controller used for dealing with content types /// [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] + [EnableOverrideAuthorization] public class MediaTypeController : ContentTypeControllerBase { /// @@ -38,35 +49,201 @@ namespace Umbraco.Web.Editors public MediaTypeController(UmbracoContext umbracoContext) : base(umbracoContext) { + } + public int GetCount() + { + return Services.ContentTypeService.CountContentTypes(); + } + + public MediaTypeDisplay GetById(int id) + { + var ct = Services.ContentTypeService.GetMediaType(id); + if (ct == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var dto = Mapper.Map(ct); + return dto; + } + + /// + /// Deletes a document type wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundType = Services.ContentTypeService.GetMediaType(id); + if (foundType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + Services.ContentTypeService.Delete(foundType, Security.CurrentUser.Id); + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Returns the avilable compositions for this content type + /// This has been wrapped in a dto instead of simple parameters to support having multiple parameters in post request body + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + [HttpPost] + public HttpResponseMessage GetAvailableCompositeMediaTypes(GetAvailableCompositionsFilter filter) + { + var result = PerformGetAvailableCompositeContentTypes(filter.ContentTypeId, UmbracoObjectTypes.MediaType, filter.FilterContentTypes, filter.FilterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); + } + + public MediaTypeDisplay GetEmpty(int parentId) + { + var ct = new MediaType(parentId); + ct.Icon = "icon-picture"; + + var dto = Mapper.Map(ct); + return dto; + } + + + /// + /// Returns all member types + /// + public IEnumerable GetAll() + { + + return Services.ContentTypeService.GetAllMediaTypes() + .Select(Mapper.Map); + } + + /// + /// Deletes a document type container wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteContainer(int id) + { + Services.ContentTypeService.DeleteMediaTypeContainer(id, Security.CurrentUser.Id); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + public HttpResponseMessage PostCreateContainer(int parentId, string name) + { + var result = Services.ContentTypeService.CreateMediaTypeContainer(parentId, name, Security.CurrentUser.Id); + + return result + ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id + : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); + } + + public MediaTypeDisplay PostSave(MediaTypeSave contentTypeSave) + { + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + saveContentType: type => Services.ContentTypeService.Save(type)); + + var display = Mapper.Map(savedCt); + + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/mediaTypeSavedHeader"), + string.Empty); + + return display; + } + + /// /// Returns the allowed child content type objects for the content item id passed in /// /// + [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] public IEnumerable GetAllowedChildren(int contentId) { - if (contentId == Core.Constants.System.RecycleBinMedia) + if (contentId == Constants.System.RecycleBinContent) return Enumerable.Empty(); - if (contentId == Core.Constants.System.Root) + IEnumerable types; + if (contentId == Constants.System.Root) { - return Services.ContentTypeService.GetAllMediaTypes() - .Where(x => x.AllowedAsRoot) - .Select(Mapper.Map); + types = Services.ContentTypeService.GetAllMediaTypes().ToList(); + + //if no allowed root types are set, just return everything + if (types.Any(x => x.AllowedAsRoot)) + types = types.Where(x => x.AllowedAsRoot); + } + else + { + var contentItem = Services.MediaService.GetById(contentId); + if (contentItem == null) + { + return Enumerable.Empty(); + } + + var ids = contentItem.ContentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + + if (ids.Any() == false) return Enumerable.Empty(); + + types = Services.ContentTypeService.GetAllMediaTypes(ids).ToList(); } - var contentItem = Services.MediaService.GetById(contentId); - if (contentItem == null) + var basics = types.Select(Mapper.Map).ToList(); + + foreach (var basic in basics) { - throw new HttpResponseException(HttpStatusCode.NotFound); + basic.Name = TranslateItem(basic.Name); + basic.Description = TranslateItem(basic.Description); } - var ids = contentItem.ContentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); - if (ids.Any() == false) return Enumerable.Empty(); + return basics; + } - return Services.ContentTypeService.GetAllMediaTypes(ids) - .Select(Mapper.Map); + /// + /// Move the media type + /// + /// + /// + public HttpResponseMessage PostMove(MoveOrCopy move) + { + return PerformMove( + move, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + doMove: (type, i) => Services.ContentTypeService.MoveMediaType(type, i)); + } + + /// + /// Copy the media type + /// + /// + /// + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + return PerformCopy( + copy, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + doCopy: (type, i) => Services.ContentTypeService.CopyMediaType(type, i)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 7d231c46f2..9d9093486f 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -6,6 +6,7 @@ using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; using System.Web.Security; @@ -72,15 +73,16 @@ namespace Umbraco.Web.Editors get { return Services.MemberService.GetMembershipScenario(); } } - public PagedResult GetPagedResults( + public PagedResult GetPagedResults( int pageNumber = 1, int pageSize = 100, string orderBy = "Name", Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, string filter = "", string memberTypeAlias = null) { - + if (pageNumber <= 0 || pageSize <= 0) { throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); @@ -89,7 +91,9 @@ namespace Umbraco.Web.Editors if (MembershipScenario == MembershipScenario.NativeUmbraco) { long totalRecords; - var members = Services.MemberService.GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, memberTypeAlias, filter).ToArray(); + var members = Services.MemberService + .GetAll((pageNumber - 1), pageSize, out totalRecords, orderBy, orderDirection, orderBySystemField + , memberTypeAlias, filter).ToArray(); if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -102,7 +106,24 @@ namespace Umbraco.Web.Editors else { int totalRecords; - var members = _provider.GetAllUsers((pageNumber - 1), pageSize, out totalRecords); + + MembershipUserCollection members; + if (filter.IsNullOrWhiteSpace()) + { + members = _provider.GetAllUsers((pageNumber - 1), pageSize, out totalRecords); + } + else + { + //we need to search! + + //try by name first + members = _provider.FindUsersByName(filter, (pageNumber - 1), pageSize, out totalRecords); + if (totalRecords == 0) + { + //try by email then + members = _provider.FindUsersByEmail(filter, (pageNumber - 1), pageSize, out totalRecords); + } + } if (totalRecords == 0) { return new PagedResult(0, 0, 0); @@ -113,7 +134,7 @@ namespace Umbraco.Web.Editors .Select(Mapper.Map); return pagedResult; } - + } /// @@ -134,7 +155,7 @@ namespace Umbraco.Web.Editors ParentId = -1 }; - TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService); + TabsAndPropertiesResolver.AddListView(display, "member", Services.DataTypeService, Services.TextService); return display; } @@ -144,6 +165,7 @@ namespace Umbraco.Web.Editors /// /// /// + [OutgoingEditorModelEvent] public MemberDisplay GetByKey(Guid key) { MembershipUser foundMembershipMember; @@ -160,23 +182,23 @@ namespace Umbraco.Web.Editors return Mapper.Map(foundMember); case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + //TODO: Support editing custom properties for members with a custom membership provider here. - //foundMember = Services.MemberService.GetByKey(key); - //if (foundMember == null) - //{ - // HandleContentNotFound(key); - //} - //foundMembershipMember = Membership.GetUser(key, false); - //if (foundMembershipMember == null) - //{ - // HandleContentNotFound(key); - //} + //foundMember = Services.MemberService.GetByKey(key); + //if (foundMember == null) + //{ + // HandleContentNotFound(key); + //} + //foundMembershipMember = Membership.GetUser(key, false); + //if (foundMembershipMember == null) + //{ + // HandleContentNotFound(key); + //} - //display = Mapper.Map(foundMembershipMember); - ////map the name over - //display.Name = foundMember.Name; - //return display; + //display = Mapper.Map(foundMembershipMember); + ////map the name over + //display.Name = foundMember.Name; + //return display; case MembershipScenario.StandaloneCustomProvider: default: @@ -195,6 +217,7 @@ namespace Umbraco.Web.Editors /// /// /// + [OutgoingEditorModelEvent] public MemberDisplay GetEmpty(string contentTypeAlias = null) { IMember emptyContent; @@ -216,7 +239,7 @@ namespace Umbraco.Web.Editors emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); return Mapper.Map(emptyContent); case MembershipScenario.CustomProviderWithUmbracoLink: - //TODO: Support editing custom properties for members with a custom membership provider here. + //TODO: Support editing custom properties for members with a custom membership provider here. case MembershipScenario.StandaloneCustomProvider: default: @@ -273,6 +296,14 @@ namespace Umbraco.Web.Editors throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); } + //We're gonna look up the current roles now because the below code can cause + // events to be raised and developers could be manually adding roles to members in + // their handlers. If we don't look this up now there's a chance we'll just end up + // removing the roles they've assigned. + var currRoles = Roles.GetRolesForUser(contentItem.PersistedContent.Username); + //find the ones to remove and remove them + var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); + string generatedPassword = null; //Depending on the action we need to first do a create or update using the membership provider // this ensures that passwords are formatted correclty and also performs the validation on the provider itself. @@ -284,6 +315,9 @@ namespace Umbraco.Web.Editors case ContentSaveAction.SaveNew: MembershipCreateStatus status; CreateWithMembershipProvider(contentItem, out status); + + // save the ID of the creator + contentItem.PersistedContent.CreatorId = Security.CurrentUser.Id; break; default: //we don't support anything else for members @@ -313,15 +347,12 @@ namespace Umbraco.Web.Editors //Now let's do the role provider stuff - now that we've saved the content item (that is important since // if we are changing the username, it must be persisted before looking up the member roles). - var currGroups = Roles.GetRolesForUser(contentItem.PersistedContent.Username); - //find the ones to remove and remove them - var toRemove = currGroups.Except(contentItem.Groups).ToArray(); - if (toRemove.Any()) + if (rolesToRemove.Any()) { - Roles.RemoveUserFromRoles(contentItem.PersistedContent.Username, toRemove); + Roles.RemoveUserFromRoles(contentItem.PersistedContent.Username, rolesToRemove); } //find the ones to add and add them - var toAdd = contentItem.Groups.Except(currGroups).ToArray(); + var toAdd = contentItem.Groups.Except(currRoles).ToArray(); if (toAdd.Any()) { //add the ones submitted @@ -339,12 +370,13 @@ namespace Umbraco.Web.Editors //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); + var localizedTextService = Services.TextService; //put the correct msgs in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: - display.AddSuccessNotification(ui.Text("speechBubbles", "editMemberSaved"), ui.Text("speechBubbles", "editMemberSaved")); + display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); break; } @@ -492,7 +524,7 @@ namespace Umbraco.Web.Editors private void RefetchMemberData(MemberSave contentItem, LookupType lookup) { var currProps = contentItem.PersistedContent.Properties.ToArray(); - + switch (MembershipScenario) { case MembershipScenario.NativeUmbraco: @@ -500,11 +532,11 @@ namespace Umbraco.Web.Editors { case LookupType.ByKey: //Go and re-fetch the persisted item - contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); + contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); break; case LookupType.ByUserName: contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); - break; + break; } break; case MembershipScenario.CustomProviderWithUmbracoLink: @@ -512,14 +544,14 @@ namespace Umbraco.Web.Editors default: var membershipUser = _provider.GetUser(contentItem.Key, false); //Go and re-fetch the persisted item - contentItem.PersistedContent = Mapper.Map(membershipUser); + contentItem.PersistedContent = Mapper.Map(membershipUser); break; } UpdateName(contentItem); //re-assign the mapped values that are not part of the membership provider properties. - var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); + var builtInAliases = Constants.Conventions.Member.GetStandardPropertyTypeStubs().Select(x => x.Key).ToArray(); foreach (var p in contentItem.PersistedContent.Properties) { var valueMapped = currProps.SingleOrDefault(x => x.Alias == p.Alias); @@ -530,7 +562,7 @@ namespace Umbraco.Web.Editors p.TagSupport.Enable = valueMapped.TagSupport.Enable; p.TagSupport.Tags = valueMapped.TagSupport.Tags; } - } + } } /// @@ -583,7 +615,7 @@ namespace Umbraco.Web.Editors contentItem.IsApproved, Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference out status); - + break; case MembershipScenario.CustomProviderWithUmbracoLink: //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use @@ -606,7 +638,7 @@ namespace Umbraco.Web.Editors case MembershipScenario.StandaloneCustomProvider: // we don't have a member type to use so we will just create the basic membership user with the provider with no // link back to the umbraco data - + var newKey = Guid.NewGuid(); //TODO: We are not supporting q/a - passing in empty here membershipUser = _provider.CreateUser( @@ -616,9 +648,9 @@ namespace Umbraco.Web.Editors "TEMP", //some membership provider's require something here even if q/a is disabled! "TEMP", //some membership provider's require something here even if q/a is disabled! contentItem.IsApproved, - newKey, + newKey, out status); - + break; default: throw new ArgumentOutOfRangeException(); @@ -673,12 +705,12 @@ namespace Umbraco.Web.Editors break; case MembershipCreateStatus.InvalidProviderUserKey: ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property + //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Invalid provider user key"), "default"); break; case MembershipCreateStatus.DuplicateProviderUserKey: ModelState.AddPropertyError( - //specify 'default' just so that it shows up as a notification - is not assigned to a property + //specify 'default' just so that it shows up as a notification - is not assigned to a property new ValidationResult("Duplicate provider user key"), "default"); break; case MembershipCreateStatus.ProviderError: diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index c47f1d0662..1d8766f253 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -2,24 +2,30 @@ using System.Linq; using System.Web.Security; using AutoMapper; +using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; +using System.Web.Http; +using System.Net; +using Umbraco.Core.PropertyEditors; +using System; +using System.Net.Http; +using ContentType = System.Net.Mime.ContentType; namespace Umbraco.Web.Editors { - //TODO: We'll need to be careful about the security on this controller, when we start implementing - // methods to modify content types we'll need to enforce security on the individual methods, we - // cannot put security on the whole controller because things like GetAllowedChildren are required for content editing. - + /// /// An API controller used for dealing with content types /// [PluginController("UmbracoApi")] - public class MemberTypeController : UmbracoAuthorizedJsonController + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] + public class MemberTypeController : ContentTypeControllerBase { /// /// Constructor @@ -42,6 +48,74 @@ namespace Umbraco.Web.Editors private readonly MembershipProvider _provider; + public MemberTypeDisplay GetById(int id) + { + var ct = Services.MemberTypeService.Get(id); + if (ct == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var dto = Mapper.Map(ct); + return dto; + } + + /// + /// Deletes a document type wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundType = Services.MemberTypeService.Get(id); + if (foundType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + Services.MemberTypeService.Delete(foundType, Security.CurrentUser.Id); + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) + { + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); + } + + public MemberTypeDisplay GetEmpty() + { + var ct = new MemberType(-1); + ct.Icon = "icon-user"; + + var dto = Mapper.Map(ct); + return dto; + } + + /// /// Returns all member types /// @@ -53,7 +127,22 @@ namespace Umbraco.Web.Editors .Select(Mapper.Map); } return Enumerable.Empty(); + } + public MemberTypeDisplay PostSave(MemberTypeSave contentTypeSave) + { + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.MemberTypeService.Get(i), + saveContentType: type => Services.MemberTypeService.Save(type)); + + var display = Mapper.Map(savedCt); + + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/memberTypeSavedHeader"), + string.Empty); + + return display; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/StylesheetController.cs b/src/Umbraco.Web/Editors/StylesheetController.cs index 38318541a4..535351a14f 100644 --- a/src/Umbraco.Web/Editors/StylesheetController.cs +++ b/src/Umbraco.Web/Editors/StylesheetController.cs @@ -22,8 +22,7 @@ namespace Umbraco.Web.Editors /// /// The API controller used for retrieving available stylesheets /// - [PluginController("UmbracoApi")] - [DisableBrowserCache] + [PluginController("UmbracoApi")] public class StylesheetController : UmbracoAuthorizedJsonController { public IEnumerable GetAll() diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index 4b81715ead..2eaf63f159 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -18,7 +18,6 @@ namespace Umbraco.Web.Editors /// The API controller used for building content queries within the template /// [PluginController("UmbracoApi")] - [DisableBrowserCache] [JsonCamelCaseFormatter] public class TemplateQueryController : UmbracoAuthorizedJsonController { diff --git a/src/Umbraco.Web/Editors/UmbracoAuthorizedJsonController.cs b/src/Umbraco.Web/Editors/UmbracoAuthorizedJsonController.cs index e60abe0859..16001057be 100644 --- a/src/Umbraco.Web/Editors/UmbracoAuthorizedJsonController.cs +++ b/src/Umbraco.Web/Editors/UmbracoAuthorizedJsonController.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Editors /// methods that are not called by Angular or don't contain a valid csrf header will NOT work. /// [ValidateAngularAntiForgeryToken] - [AngularJsonOnlyConfiguration] + [AngularJsonOnlyConfiguration] public abstract class UmbracoAuthorizedJsonController : UmbracoAuthorizedApiController { protected UmbracoAuthorizedJsonController() diff --git a/src/Umbraco.Web/Editors/ValidationHelper.cs b/src/Umbraco.Web/Editors/ValidationHelper.cs index 7bad8ceb8c..e1633fa6bf 100644 --- a/src/Umbraco.Web/Editors/ValidationHelper.cs +++ b/src/Umbraco.Web/Editors/ValidationHelper.cs @@ -1,12 +1,31 @@ using System; using System.ComponentModel; using System.Linq; +using System.Web; +using System.Web.Http.ModelBinding; +using Umbraco.Core; using Umbraco.Core.Models.Validation; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors { internal class ValidationHelper { + internal static void ValidateEditorModelWithResolver(ModelStateDictionary modelState, object model) + { + var validationResult = EditorValidationResolver.Current.Validate(model); + foreach (var vr in validationResult + .WhereNotNull() + .Where(x => x.ErrorMessage.IsNullOrWhiteSpace() == false) + .Where(x => x.MemberNames.Any())) + { + foreach (var memberName in vr.MemberNames) + { + modelState.AddModelError(memberName, vr.ErrorMessage); + } + } + } + /// /// This will check if any properties of the model are attributed with the RequiredForPersistenceAttribute attribute and if they are it will /// check if that property validates, if it doesn't it means that the current model cannot be persisted because it doesn't have the necessary information diff --git a/src/Umbraco.Web/GridTemplateExtensions.cs b/src/Umbraco.Web/GridTemplateExtensions.cs index ca75552e51..cf267897d4 100644 --- a/src/Umbraco.Web/GridTemplateExtensions.cs +++ b/src/Umbraco.Web/GridTemplateExtensions.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web } - //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + [Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] public static MvcHtmlString GetGridHtml(this IPublishedProperty property, string framework = "bootstrap3") { var asString = property.Value as string; @@ -97,13 +97,13 @@ namespace Umbraco.Web return htmlHelper.GetGridHtml(property, framework); } - //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + [Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem) { return GetGridHtml(contentItem, "bodyText", "bootstrap3"); } - //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + [Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias) { Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); @@ -111,7 +111,7 @@ namespace Umbraco.Web return GetGridHtml(contentItem, propertyAlias, "bootstrap3"); } - //[Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] + [Obsolete("This should not be used, GetGridHtml methods accepting HtmlHelper as a parameter or GetGridHtml extensions on HtmlHelper should be used instead")] public static MvcHtmlString GetGridHtml(this IPublishedContent contentItem, string propertyAlias, string framework) { Mandate.ParameterNotNullOrEmpty(propertyAlias, "propertyAlias"); @@ -127,7 +127,7 @@ namespace Umbraco.Web return htmlHelper.GetGridHtml(contentItem, propertyAlias, framework); } - //[Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")] + [Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")] private static HtmlHelper CreateHtmlHelper(object model) { var cc = new ControllerContext diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 75e628851e..71fc3be8f1 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -3,12 +3,18 @@ using System.Linq; using System.Text; using System.Web; using System.Web.Mvc; +using ClientDependency.Core.Config; using Microsoft.Owin.Security; using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Web.Editors; +using Umbraco.Web.Models; namespace Umbraco.Web { + using Umbraco.Core.Configuration; + /// /// HtmlHelper extensions for the back office /// @@ -30,6 +36,7 @@ namespace Umbraco.Web /// public static IHtmlString BareMinimumServerVariablesScript(this HtmlHelper html, UrlHelper uri, string externalLoginsUrl) { + var version = UmbracoVersion.GetSemanticVersion().ToSemanticString(); var str = @"" + + @"" + + @"" + + @"" + + @""; + + string noPreviewLinks = @""; + + // Get page value + int pageId = umbCtx.PublishedContentRequest.UmbracoPage.PageID; + string[] path = umbCtx.PublishedContentRequest.UmbracoPage.SplitPath; + string result = string.Empty; + string cssPath = CanvasDesignerUtility.GetStylesheetPath(path, false); + + if (umbCtx.InPreviewMode) + { + canvasdesignerConfigPath = string.IsNullOrEmpty(canvasdesignerConfigPath) == false + ? canvasdesignerConfigPath + : string.Format("{0}/js/canvasdesigner.config.js", umbracoPath); + canvasdesignerPalettesPath = string.IsNullOrEmpty(canvasdesignerPalettesPath) == false + ? canvasdesignerPalettesPath + : string.Format("{0}/js/canvasdesigner.palettes.js", umbracoPath); + + if (string.IsNullOrEmpty(cssPath) == false) + result = string.Format(noPreviewLinks, cssPath) + Environment.NewLine; + + result = result + string.Format(previewLink, umbracoPath, canvasdesignerConfigPath, canvasdesignerPalettesPath, pageId); + } + else + { + // Get css path for current page + if (string.IsNullOrEmpty(cssPath) == false) + result = string.Format(noPreviewLinks, cssPath); + } + + return new HtmlString(result); + + } + + #endregion + + } } diff --git a/src/Umbraco.Web/HttpCookieExtensions.cs b/src/Umbraco.Web/HttpCookieExtensions.cs index dc6d087fb3..7aec1466da 100644 --- a/src/Umbraco.Web/HttpCookieExtensions.cs +++ b/src/Umbraco.Web/HttpCookieExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Web; +using Microsoft.Owin; using Umbraco.Core; namespace Umbraco.Web @@ -62,6 +63,16 @@ namespace Umbraco.Web return request.Cookies[Constants.Web.PreviewCookieName] != null; } + /// + /// Does a preview cookie exist ? + /// + /// + /// + public static bool HasPreviewCookie(this IOwinRequest request) + { + return request.Cookies[Constants.Web.PreviewCookieName] != null; + } + /// /// Does a cookie exist with the specified key ? /// diff --git a/src/Umbraco.Web/HttpRequestExtensions.cs b/src/Umbraco.Web/HttpRequestExtensions.cs index e700f96571..d7f9e409c8 100644 --- a/src/Umbraco.Web/HttpRequestExtensions.cs +++ b/src/Umbraco.Web/HttpRequestExtensions.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web /// public static string GetItemAsString(this HttpRequest request, string key, string valueIfNotFound = "") { - return new HttpRequestWrapper(request).GetItemAsString(key); + return new HttpRequestWrapper(request).GetItemAsString(key, valueIfNotFound); } /// @@ -44,7 +44,7 @@ namespace Umbraco.Web /// public static string GetItemAsString(this HttpRequestBase request, string key, string valueIfNotFound = "") { - var val = HttpContext.Current.Request[key]; + var val = request[key]; return !val.IsNullOrWhiteSpace() ? val : valueIfNotFound; } @@ -57,7 +57,7 @@ namespace Umbraco.Web /// public static T GetItemAs(this HttpRequestBase request, string key) { - var val = HttpContext.Current.Request[key]; + var val = request[key]; var whitespaceCheck = !val.IsNullOrWhiteSpace() ? val : string.Empty; if (whitespaceCheck.IsNullOrWhiteSpace()) return (T) typeof (T).GetDefaultValue(); diff --git a/src/Umbraco.Web/IHttpContextAccessor.cs b/src/Umbraco.Web/IHttpContextAccessor.cs new file mode 100644 index 0000000000..068783725a --- /dev/null +++ b/src/Umbraco.Web/IHttpContextAccessor.cs @@ -0,0 +1,15 @@ +using System.Web; + +namespace Umbraco.Web +{ + /// + /// Used to retrieve the HttpContext + /// + /// + /// NOTE: This has a singleton lifespan + /// + public interface IHttpContextAccessor + { + HttpContextBase Value { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/IUmbracoContextAccessor.cs b/src/Umbraco.Web/IUmbracoContextAccessor.cs index e3614a1e86..8111606602 100644 --- a/src/Umbraco.Web/IUmbracoContextAccessor.cs +++ b/src/Umbraco.Web/IUmbracoContextAccessor.cs @@ -4,9 +4,9 @@ namespace Umbraco.Web /// Used to retrieve the Umbraco context /// /// - /// TODO: We could expose this to make working with UmbracoContext easier if we were to use it throughout the codebase - /// - internal interface IUmbracoContextAccessor + /// NOTE: This has a singleton lifespan + /// + public interface IUmbracoContextAccessor { UmbracoContext Value { get; } } diff --git a/src/Umbraco.Web/ImageCropperBaseExtensions.cs b/src/Umbraco.Web/ImageCropperBaseExtensions.cs index b870335c91..e993559f1e 100644 --- a/src/Umbraco.Web/ImageCropperBaseExtensions.cs +++ b/src/Umbraco.Web/ImageCropperBaseExtensions.cs @@ -1,38 +1,18 @@ -namespace Umbraco.Web +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.Models; + +namespace Umbraco.Web { - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text; - - using Newtonsoft.Json; - - using Umbraco.Core; - using Umbraco.Core.Logging; - using Umbraco.Web.Models; - internal static class ImageCropperBaseExtensions { - - internal static ImageCropData GetImageCrop(this string json, string id) - { - var ic = new ImageCropData(); - if (json.DetectIsJson()) - { - try - { - var imageCropperSettings = JsonConvert.DeserializeObject>(json); - ic = imageCropperSettings.GetCrop(id); - } - catch (Exception ex) - { - LogHelper.Error(typeof(ImageCropperBaseExtensions), "Could not parse the json string: " + json, ex); - } - } - return ic; - } - internal static ImageCropDataSet SerializeToCropDataSet(this string json) { var imageCrops = new ImageCropDataSet(); @@ -40,7 +20,11 @@ { try { - imageCrops = JsonConvert.DeserializeObject(json); + imageCrops = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }); } catch (Exception ex) { @@ -61,13 +45,14 @@ internal static ImageCropData GetCrop(this IEnumerable dataset, string cropAlias) { - if (dataset == null || !dataset.Any()) + var imageCropDatas = dataset.ToArray(); + if (dataset == null || imageCropDatas.Any() == false) return null; if (string.IsNullOrEmpty(cropAlias)) - return dataset.FirstOrDefault(); + return imageCropDatas.FirstOrDefault(); - return dataset.FirstOrDefault(x => x.Alias.ToLowerInvariant() == cropAlias.ToLowerInvariant()); + return imageCropDatas.FirstOrDefault(x => x.Alias.ToLowerInvariant() == cropAlias.ToLowerInvariant()); } internal static string GetCropBaseUrl(this ImageCropDataSet cropDataSet, string cropAlias, bool preferFocalPoint) diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index a76b39f187..bcdea6eb03 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -1,12 +1,13 @@ -namespace Umbraco.Web +using System; +using Newtonsoft.Json.Linq; +using System.Globalization; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models; + +namespace Umbraco.Web { - using System.Globalization; - using System.Text; - - using Umbraco.Core; - using Umbraco.Core.Models; - using Umbraco.Web.Models; - /// /// Provides extension methods for getting ImageProcessor Url from the core Image Cropper property editor /// @@ -113,31 +114,43 @@ ImageCropRatioMode? ratioMode = null, bool upScale = true) { - string imageCropperValue = null; - - string mediaItemUrl; - - if (mediaItem.HasProperty(propertyAlias) && mediaItem.HasValue(propertyAlias)) - { - imageCropperValue = mediaItem.GetPropertyValue(propertyAlias); - - // get the raw value (this will be json) - var urlValue = mediaItem.GetPropertyValue(propertyAlias); - - mediaItemUrl = urlValue.DetectIsJson() - ? urlValue.SerializeToCropDataSet().Src - : urlValue; - } - else - { - mediaItemUrl = mediaItem.Url; - } + if (mediaItem == null) throw new ArgumentNullException("mediaItem"); var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; - return mediaItemUrl != null - ? GetCropUrl(mediaItemUrl, width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale) - : string.Empty; + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + return string.Empty; + + //get the default obj from the value converter + var cropperValue = mediaItem.GetPropertyValue(propertyAlias); + + //is it strongly typed? + var stronglyTyped = cropperValue as ImageCropDataSet; + string mediaItemUrl; + if (stronglyTyped != null) + { + mediaItemUrl = stronglyTyped.Src; + return GetCropUrl( + mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //this shouldn't be the case but we'll check + var jobj = cropperValue as JObject; + if (jobj != null) + { + stronglyTyped = jobj.ToObject(); + mediaItemUrl = stronglyTyped.Src; + return GetCropUrl( + mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //it's a single string + mediaItemUrl = cropperValue.ToString(); + return GetCropUrl( + mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); } /// @@ -203,49 +216,73 @@ string furtherOptions = null, ImageCropRatioMode? ratioMode = null, bool upScale = true) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageCropDataSet cropDataSet = null; + if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + cropDataSet = imageCropperValue.SerializeToCropDataSet(); + } + return GetCropUrl( + imageUrl, cropDataSet, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + public static string GetCropUrl( + this string imageUrl, + ImageCropDataSet cropDataSet, + int? width = null, + int? height = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) { if (string.IsNullOrEmpty(imageUrl) == false) { var imageProcessorUrl = new StringBuilder(); - if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) { - var cropDataSet = imageCropperValue.SerializeToCropDataSet(); - if (cropDataSet != null) + var crop = cropDataSet.GetCrop(cropAlias); + + imageProcessorUrl.Append(cropDataSet.Src); + + var cropBaseUrl = cropDataSet.GetCropBaseUrl(cropAlias, preferFocalPoint); + if (cropBaseUrl != null) { - var crop = cropDataSet.GetCrop(cropAlias); + imageProcessorUrl.Append(cropBaseUrl); + } + else + { + return null; + } - imageProcessorUrl.Append(cropDataSet.Src); + if (crop != null & useCropDimensions) + { + width = crop.Width; + height = crop.Height; + } - var cropBaseUrl = cropDataSet.GetCropBaseUrl(cropAlias, preferFocalPoint); - if (cropBaseUrl != null) - { - imageProcessorUrl.Append(cropBaseUrl); - } - else - { - return null; - } + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) + { + var heightRatio = (decimal)crop.Height / (decimal)crop.Width; + imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); + } - if (crop != null & useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) - { - var heightRatio = (decimal)crop.Height / (decimal)crop.Width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) - { - var widthRatio = (decimal)crop.Width / (decimal)crop.Height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) + { + var widthRatio = (decimal)crop.Width / (decimal)crop.Height; + imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); } } else @@ -306,7 +343,7 @@ if (upScale == false) { - imageProcessorUrl.Append("&upscale=false"); + imageProcessorUrl.Append("&upscale=false"); } if (furtherOptions != null) diff --git a/src/Umbraco.Web/Install/HttpInstallAuthorizeAttribute.cs b/src/Umbraco.Web/Install/HttpInstallAuthorizeAttribute.cs index bab2bddc40..4559a42740 100644 --- a/src/Umbraco.Web/Install/HttpInstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web/Install/HttpInstallAuthorizeAttribute.cs @@ -5,6 +5,7 @@ using System.Web.Http.Controllers; using Umbraco.Core; using Umbraco.Web.Security; using umbraco.BasePages; +using Umbraco.Core.Logging; namespace Umbraco.Web.Install { @@ -52,6 +53,7 @@ namespace Umbraco.Web.Install return true; } var umbCtx = GetUmbracoContext(); + //otherwise we need to ensure that a user is logged in var isLoggedIn = GetUmbracoContext().Security.ValidateCurrentUser(); if (isLoggedIn) @@ -60,8 +62,9 @@ namespace Umbraco.Web.Install } return false; } - catch (Exception) + catch (Exception ex) { + LogHelper.Error("An error occurred determining authorization", ex); return false; } } diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index 59e073dc8c..d39b16f6ac 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Install { return new List { - new NewInstallStep(_umbContext.Application), + new NewInstallStep(_umbContext.HttpContext, _umbContext.Application), new UpgradeStep(), new FilePermissionsStep(), new MajorVersion7UpgradeReport(_umbContext.Application), @@ -103,9 +103,9 @@ namespace Umbraco.Web.Install string userAgent = _umbContext.HttpContext.Request.UserAgent; // Check for current install Id - Guid installId = Guid.NewGuid(); - StateHelper.Cookies.Cookie installCookie = new StateHelper.Cookies.Cookie("umb_installId", 1); - if (!String.IsNullOrEmpty(installCookie.GetValue())) + var installId = Guid.NewGuid(); + var installCookie = new StateHelper.Cookies.Cookie("umb_installId", 1); + if (string.IsNullOrEmpty(installCookie.GetValue()) == false) { if (Guid.TryParse(installCookie.GetValue(), out installId)) { @@ -116,13 +116,13 @@ namespace Umbraco.Web.Install } installCookie.SetValue(installId.ToString()); - string dbProvider = String.Empty; - if (!IsBrandNewInstall) + string dbProvider = string.Empty; + if (IsBrandNewInstall == false) dbProvider = ApplicationContext.Current.DatabaseContext.DatabaseProvider.ToString(); org.umbraco.update.CheckForUpgrade check = new org.umbraco.update.CheckForUpgrade(); check.Install(installId, - !IsBrandNewInstall, + IsBrandNewInstall == false, isCompleted, DateTime.Now, UmbracoVersion.Current.Major, @@ -135,7 +135,7 @@ namespace Umbraco.Web.Install } catch (Exception ex) { - + LogHelper.Error("An error occurred in InstallStatus trying to check upgrades", ex); } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index baa94c304e..4d8e9c5a4b 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration; +using System.Web; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -22,10 +23,12 @@ namespace Umbraco.Web.Install.InstallSteps "User", 20, "")] internal class NewInstallStep : InstallSetupStep { + private readonly HttpContextBase _http; private readonly ApplicationContext _applicationContext; - public NewInstallStep(ApplicationContext applicationContext) + public NewInstallStep(HttpContextBase http, ApplicationContext applicationContext) { + _http = http; _applicationContext = applicationContext; } @@ -111,7 +114,7 @@ namespace Umbraco.Web.Install.InstallSteps //the continue install UI : "continueinstall"; } } - + public override bool RequiresExecution(UserModel model) { //now we have to check if this is really a new install, the db might be configured and might contain data @@ -136,6 +139,10 @@ namespace Umbraco.Web.Install.InstallSteps } else { + // In this one case when it's a brand new install and nothing has been configured, make sure the + // back office cookie is cleared so there's no old cookies lying around causing problems + _http.ExpireCookie(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); + return true; } } diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index b8be5b7a22..78fe6d3766 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -27,6 +27,20 @@ namespace Umbraco.Web.Install.InstallSteps public override InstallSetupResult Execute(object model) { + var ih = new InstallHelper(UmbracoContext.Current); + + //During a new install we'll log the default user in (which is id = 0). + // During an upgrade, the user will already need to be logged in in order to run the installer. + + var security = new WebSecurity(_httpContext, _applicationContext); + //we do this check here because for upgrades the user will already be logged in, for brand new installs, + // they will not be logged in, however we cannot check the current installation status because it will tell + // us that it is in 'upgrade' because we already have a database conn configured and a database. + if (security.IsAuthenticated() == false && GlobalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) + { + security.PerformLogin(0); + } + //This is synonymous with library.RefreshContent() - but we don't want to use library // for anything anymore so welll use the method that it is wrapping. This will just make sure // the correct xml structure exists in the xml cache file. This is required by some upgrade scripts @@ -39,18 +53,8 @@ namespace Umbraco.Web.Install.InstallSteps // Update ClientDependency version var clientDependencyConfig = new ClientDependencyConfiguration(_applicationContext.ProfilingLogger.Logger); var clientDependencyUpdated = clientDependencyConfig.IncreaseVersionNumber(); - - var security = new WebSecurity(_httpContext, _applicationContext); - security.PerformLogin(0); - - ////Clear the auth cookie - this is required so that the login screen is displayed after upgrade and so the - //// csrf anti-forgery tokens are created, otherwise there will just be JS errors if the user has an old - //// login token from a previous version when we didn't have csrf tokens in place - //var security = new WebSecurity(new HttpContextWrapper(Context), ApplicationContext.Current); - //security.ClearCurrentLogin(); - - //reports the ended install - InstallHelper ih = new InstallHelper(UmbracoContext.Current); + + //reports the ended install ih.InstallStatus(true, ""); return null; diff --git a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs index b4171add35..10d105883f 100644 --- a/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web/Macros/PartialViewMacroEngine.cs @@ -124,7 +124,7 @@ namespace Umbraco.Web.Macros var routeVals = new RouteData(); routeVals.Values.Add("controller", "PartialViewMacro"); routeVals.Values.Add("action", "Index"); - routeVals.DataTokens.Add("umbraco-context", umbCtx); //required for UmbracoViewPage + routeVals.DataTokens.Add(Umbraco.Core.Constants.Web.UmbracoContextDataToken, umbCtx); //required for UmbracoViewPage //lets render this controller as a child action var viewContext = new ViewContext {ViewData = new ViewDataDictionary()};; diff --git a/src/Umbraco.Web/Media/ImageUrl.cs b/src/Umbraco.Web/Media/ImageUrl.cs index 8f56d8bfa4..dff9358a38 100644 --- a/src/Umbraco.Web/Media/ImageUrl.cs +++ b/src/Umbraco.Web/Media/ImageUrl.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web.Media private static object GetContentFromCache(int nodeIdInt, string field) { - var content = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var content = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( string.Format("{0}{1}_{2}", CacheKeys.ContentItemCacheKey, nodeIdInt.ToString(CultureInfo.InvariantCulture), field)); return content; } diff --git a/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs b/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs index a2777c0137..6f9b26c79d 100644 --- a/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs +++ b/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Media.ThumbnailProviders return false; // Make sure the thumbnail exists - var tmpThumbUrl = fileUrl.Replace(ext, "_thumb.jpg"); + var tmpThumbUrl = fileUrl.Replace(ext, "_thumb" + ext); try { diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 08868e4e5f..5df479ce76 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web if (!result.MemberNames.Any()) { //add a model state error for the entire property - modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage); + modelState.AddModelError(string.Format("{0}.{1}", "_Properties", propertyAlias), result.ErrorMessage); } else { @@ -72,7 +72,7 @@ namespace Umbraco.Web // so that we can try to match it up to a real sub field of this editor foreach (var field in result.MemberNames) { - modelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", propertyAlias, field), result.ErrorMessage); + modelState.AddModelError(string.Format("{0}.{1}.{2}", "_Properties", propertyAlias, field), result.ErrorMessage); } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs index 3a13a85f78..87de7b435c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Runtime.Serialization; using Umbraco.Core.Models; @@ -30,6 +31,7 @@ namespace Umbraco.Web.Models.ContentEditing /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// [DataMember(Name = "notifications")] + [ReadOnly(true)] public List Notifications { get; private set; } /// @@ -43,6 +45,7 @@ namespace Umbraco.Web.Models.ContentEditing /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. /// [DataMember(Name = "ModelState")] + [ReadOnly(true)] public IDictionary Errors { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs index a563910d4f..343b018000 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs @@ -34,6 +34,6 @@ namespace Umbraco.Web.Models.ContentEditing public bool HideLabel { get; set; } [DataMember(Name = "validation")] - public PropertyTypeValidation Validation { get; set; } + public PropertyTypeValidation Validation { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs index 2e527cfd93..8dde1f1f97 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs @@ -1,7 +1,11 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Models.Validation; namespace Umbraco.Web.Models.ContentEditing { @@ -14,6 +18,21 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "contentType", Namespace = "")] public class ContentTypeBasic : EntityBasic { + /// + /// Overridden to apply our own validation attributes since this is not always required for other classes + /// + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public override string Alias { get; set; } + + [DataMember(Name = "updateDate")] + [ReadOnly(true)] + public DateTime UpdateDate { get; set; } + + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } [DataMember(Name = "description")] public string Description { get; set; } @@ -25,6 +44,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns true if the icon represents a CSS class instead of a file path /// [DataMember(Name = "iconIsClass")] + [ReadOnly(true)] public bool IconIsClass { get @@ -42,13 +62,14 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns the icon file path if the icon is not a class, otherwise returns an empty string /// [DataMember(Name = "iconFilePath")] + [ReadOnly(true)] public string IconFilePath { get { return IconIsClass - ? string.Empty - : IOHelper.ResolveUrl("~/umbraco/images/umbraco/" + Icon); + ? string.Empty + : string.Format("{0}images/umbraco/{1}", GlobalSettings.Path.EnsureEndsWith("/"), Icon); } } @@ -56,6 +77,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns true if the icon represents a CSS class instead of a file path /// [DataMember(Name = "thumbnailIsClass")] + [ReadOnly(true)] public bool ThumbnailIsClass { get @@ -73,6 +95,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns the icon file path if the icon is not a class, otherwise returns an empty string /// [DataMember(Name = "thumbnailFilePath")] + [ReadOnly(true)] public string ThumbnailFilePath { get @@ -83,4 +106,4 @@ namespace Umbraco.Web.Models.ContentEditing } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs new file mode 100644 index 0000000000..fb50fdec5c --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models.Validation; + +namespace Umbraco.Web.Models.ContentEditing +{ + public abstract class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel + { + protected ContentTypeCompositionDisplay() + { + //initialize collections so at least their never null + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); + Notifications = new List(); + } + + //name, alias, icon, thumb, desc, inherited from basic + + //List view + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } + + [DataMember(Name = "listViewEditorName")] + [ReadOnly(true)] + public string ListViewEditorName { get; set; } + + //Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable AllowedContentTypes { get; set; } + + //Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + //Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable LockedCompositeContentTypes { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + [ReadOnly(true)] + public List Notifications { get; private set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will + /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the + /// updated model. + /// + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. + /// + [DataMember(Name = "ModelState")] + [ReadOnly(true)] + public IDictionary Errors { get; set; } + } + + [DataContract(Name = "contentType", Namespace = "")] + public abstract class ContentTypeCompositionDisplay : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay + { + protected ContentTypeCompositionDisplay() + { + //initialize collections so at least their never null + Groups = new List>(); + } + + //Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs new file mode 100644 index 0000000000..62e1979076 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Umbraco.Core; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Abstract model used to save content types + /// + [DataContract(Name = "contentType", Namespace = "")] + public abstract class ContentTypeSave : ContentTypeBasic, IValidatableObject + { + protected ContentTypeSave() + { + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); + } + + //Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + //Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable AllowedContentTypes { get; set; } + + /// + /// Custom validation + /// + /// + /// + public virtual IEnumerable Validate(ValidationContext validationContext) + { + if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) + yield return new ValidationResult("Composite Content Type value cannot be null", new[] {"CompositeContentTypes"}); + } + } + + /// + /// Abstract model used to save content types + /// + /// + [DataContract(Name = "contentType", Namespace = "")] + public abstract class ContentTypeSave : ContentTypeSave + where TPropertyType : PropertyTypeBasic + { + protected ContentTypeSave() + { + Groups = new List>(); + } + + //Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) + { + foreach (var validationResult in base.Validate(validationContext)) + { + yield return validationResult; + } + + var duplicateGroups = Groups.GroupBy(x => x.Name).Where(x => x.Count() > 1).ToArray(); + if (duplicateGroups.Any()) + { + //we need to return the field name with an index so it's wired up correctly + var lastIndex = Groups.IndexOf(duplicateGroups.Last().Last()); + yield return new ValidationResult("Duplicate group names not allowed", new[] + { + string.Format("Groups[{0}].Name", lastIndex) + }); + } + + var duplicateProperties = Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Where(x => x.Count() > 1).ToArray(); + if (duplicateProperties.Any()) + { + //we need to return the field name with an index so it's wired up correctly + var lastProperty = duplicateProperties.Last().Last(); + var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty)); + + yield return new ValidationResult("Duplicate property aliases not allowed: " + lastProperty.Alias, new[] + { + string.Format("Groups[{0}].Properties[{1}].Alias", propertyGroup.SortOrder, lastProperty.SortOrder) + }); + } + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeBasic.cs index 0efb67f567..1165ec8fd6 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DataTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeBasic.cs @@ -15,5 +15,13 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "isSystem")] [ReadOnly(true)] public bool IsSystemDataType { get; set; } + + [DataMember(Name = "group")] + [ReadOnly(true)] + public string Group { get; set; } + + [DataMember(Name = "hasPrevalues")] + [ReadOnly(true)] + public bool HasPrevalues { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs new file mode 100644 index 0000000000..65641e7f63 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "contentType", Namespace = "")] + public class DocumentTypeDisplay : ContentTypeCompositionDisplay + { + public DocumentTypeDisplay() + { + //initialize collections so at least their never null + AllowedTemplates = new List(); + } + + //name, alias, icon, thumb, desc, inherited from the content type + + // Templates + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } + + [DataMember(Name = "defaultTemplate")] + public EntityBasic DefaultTemplate { get; set; } + + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeSave.cs new file mode 100644 index 0000000000..cdf20dba8d --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeSave.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Umbraco.Core; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Model used to save a document type + /// + [DataContract(Name = "contentType", Namespace = "")] + public class DocumentTypeSave : ContentTypeSave + { + /// + /// The list of allowed templates to assign (template alias) + /// + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } + + /// + /// The default template to assign (template alias) + /// + [DataMember(Name = "defaultTemplate")] + public string DefaultTemplate { get; set; } + + /// + /// Custom validation + /// + /// + /// + public override IEnumerable Validate(ValidationContext validationContext) + { + if (AllowedTemplates.Any(x => StringExtensions.IsNullOrWhiteSpace(x))) + yield return new ValidationResult("Template value cannot be null", new[] { "AllowedTemplates" }); + + foreach (var v in base.Validate(validationContext)) + { + yield return v; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs index 520ff677b0..40d884d653 100644 --- a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; @@ -29,6 +30,7 @@ namespace Umbraco.Web.Models.ContentEditing public string Icon { get; set; } [DataMember(Name = "trashed")] + [ReadOnly(true)] public bool Trashed { get; set; } /// @@ -44,8 +46,11 @@ namespace Umbraco.Web.Models.ContentEditing /// /// This will only be populated for some entities like macros /// + /// + /// This is overrideable to specify different validation attributes if required + /// [DataMember(Name = "alias")] - public string Alias { get; set; } + public virtual string Alias { get; set; } /// /// The path of the entity @@ -57,6 +62,7 @@ namespace Umbraco.Web.Models.ContentEditing /// A collection of extra data that is available for this specific entity/entity type /// [DataMember(Name = "metaData")] + [ReadOnly(true)] public IDictionary AdditionalData { get; private set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs new file mode 100644 index 0000000000..d9bc169c8f --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/GetAvailableCompositionsFilter.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Web.Models.ContentEditing +{ + public class GetAvailableCompositionsFilter + { + public int ContentTypeId { get; set; } + public string[] FilterPropertyTypes { get; set; } + public string[] FilterContentTypes { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MacroParameter.cs b/src/Umbraco.Web/Models/ContentEditing/MacroParameter.cs index a5cf8733c3..2ca9d0fa90 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MacroParameter.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MacroParameter.cs @@ -20,6 +20,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name")] public string Name { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } /// /// The editor view to render for this parameter diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs new file mode 100644 index 0000000000..7063613d66 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaTypeDisplay.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "contentType", Namespace = "")] + public class MediaTypeDisplay : ContentTypeCompositionDisplay + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/MediaTypeSave.cs new file mode 100644 index 0000000000..e20eb23125 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaTypeSave.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Model used to save a media type + /// + [DataContract(Name = "contentType", Namespace = "")] + public class MediaTypeSave : ContentTypeSave + { + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs new file mode 100644 index 0000000000..dfe98461dd --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeBasic.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Basic member property type + /// + [DataContract(Name = "contentType", Namespace = "")] + public class MemberPropertyTypeBasic : PropertyTypeBasic + { + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } + + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs new file mode 100644 index 0000000000..8f7d4be310 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberPropertyTypeDisplay.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyType")] + public class MemberPropertyTypeDisplay : PropertyTypeDisplay + { + [DataMember(Name = "showOnMemberProfile")] + public bool MemberCanViewProperty { get; set; } + + [DataMember(Name = "memberCanEdit")] + public bool MemberCanEditProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberTypeDisplay.cs new file mode 100644 index 0000000000..e6092a10f4 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberTypeDisplay.cs @@ -0,0 +1,10 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "contentType", Namespace = "")] + public class MemberTypeDisplay : ContentTypeCompositionDisplay + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/MemberTypeSave.cs new file mode 100644 index 0000000000..98bfbf0fce --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MemberTypeSave.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Model used to save a member type + /// + public class MemberTypeSave : ContentTypeSave + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs b/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs index a2cc45c446..a387876a45 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MoveOrCopy.cs @@ -33,6 +33,13 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "relateToOriginal", IsRequired = true)] [Required] public bool RelateToOriginal { get; set; } + + /// + /// Boolean indicating whether copying the object should be recursive + /// + [DataMember(Name = "recursive", IsRequired = true)] + [Required] + public bool Recursive { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyEditorBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyEditorBasic.cs index 17df4fde63..50b8641b19 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyEditorBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyEditorBasic.cs @@ -14,5 +14,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name")] public string Name { get; set; } + + [DataMember(Name = "icon")] + public string Icon { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs new file mode 100644 index 0000000000..6bfe1dc76e --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyGroup", Namespace = "")] + public abstract class PropertyGroupBasic + { + /// + /// Gets the special generic properties tab identifier. + /// + public const int GenericPropertiesGroupId = -666; + + /// + /// Gets a value indicating whether this tab is the generic properties tab. + /// + [IgnoreDataMember] + public bool IsGenericProperties { get { return Id == GenericPropertiesGroupId; } } + + /// + /// Gets a value indicating whether the property group is inherited through + /// content types composition. + /// + /// A property group can be inherited and defined on the content type + /// currently being edited, at the same time. Inherited is true when there exists at least + /// one property group higher in the composition, with the same alias. + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } + + // needed - so we can handle alias renames + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [Required] + [DataMember(Name = "name")] + public string Name { get; set; } + } + + [DataContract(Name = "propertyGroup", Namespace = "")] + public class PropertyGroupBasic : PropertyGroupBasic + where TPropertyType: PropertyTypeBasic + { + public PropertyGroupBasic() + { + Properties = new List(); + } + + + + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs new file mode 100644 index 0000000000..c31c93b206 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyGroup", Namespace = "")] + public class PropertyGroupDisplay : PropertyGroupBasic + where TPropertyTypeDisplay : PropertyTypeDisplay + { + public PropertyGroupDisplay() + { + Properties = new List(); + ParentTabContentTypeNames = new List(); + ParentTabContentTypes = new List(); + } + + /// + /// Gets the context content type. + /// + [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] + public int ContentTypeId { get; set; } + + /// + /// Gets the identifiers of the content types that define this group. + /// + [DataMember(Name = "parentTabContentTypes")] + [ReadOnly(true)] + public IEnumerable ParentTabContentTypes { get; set; } + + /// + /// Gets the name of the content types that define this group. + /// + [DataMember(Name = "parentTabContentTypeNames")] + [ReadOnly(true)] + public IEnumerable ParentTabContentTypeNames { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs new file mode 100644 index 0000000000..107ae287fa --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -0,0 +1,50 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyType")] + public class PropertyTypeBasic + { + /// + /// Gets a value indicating whether the property type is inherited through + /// content types composition. + /// + /// Inherited is true when the property is defined by a content type + /// higher in the composition, and not by the content type currently being + /// edited. + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } + + // needed - so we can handle alias renames + [DataMember(Name = "id")] + public int Id { get; set; } + + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "validation")] + public PropertyTypeValidation Validation { get; set; } + + [DataMember(Name = "label")] + [Required] + public string Label { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [DataMember(Name = "dataTypeId")] + [Required] + public int DataTypeId { get; set; } + + //SD: Is this really needed ? + [DataMember(Name = "groupId")] + public int GroupId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs new file mode 100644 index 0000000000..4ee7c3eced --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyType")] + public class PropertyTypeDisplay : PropertyTypeBasic + { + [DataMember(Name = "editor")] + [ReadOnly(true)] + public string Editor { get; set; } + + [DataMember(Name = "view")] + [ReadOnly(true)] + public string View { get; set; } + + [DataMember(Name = "config")] + [ReadOnly(true)] + public IDictionary Config { get; set; } + + /// + /// Gets a value indicating whether this property should be locked when editing. + /// + /// This is used for built in properties like the default MemberType + /// properties that should not be editable from the backoffice. + [DataMember(Name = "locked")] + [ReadOnly(true)] + public bool Locked { get; set; } + + /// + /// This is required for the UI editor to know if this particular property belongs to + /// an inherited item or the current item. + /// + [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] + public int ContentTypeId { get; set; } + + /// + /// This is required for the UI editor to know which content type name this property belongs + /// to based on the property inheritance structure + /// + [DataMember(Name = "contentTypeName")] + [ReadOnly(true)] + public string ContentTypeName { get; set; } + + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/RichTextEditorCommand.cs b/src/Umbraco.Web/Models/ContentEditing/RichTextEditorCommand.cs index 55a1bd9331..fb2a95c2a4 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RichTextEditorCommand.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RichTextEditorCommand.cs @@ -10,6 +10,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "richtexteditorcommand", Namespace = "")] public class RichTextEditorCommand { + [DataMember(Name = "name")] + public string Name { get; set; } + [DataMember(Name = "icon")] public string Icon { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs b/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs index 9c4ce6e9a4..83b00662c4 100644 --- a/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs +++ b/src/Umbraco.Web/Models/ContentEditing/SimpleNotificationModel.cs @@ -21,5 +21,11 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "notifications")] public List Notifications { get; private set; } + + /// + /// A default msg + /// + [DataMember(Name = "message")] + public string Message { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 37fe9151b0..cfee14a2a8 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -48,6 +48,8 @@ namespace Umbraco.Web.Models ? null // for tests only : umbracoContext.ContentCache.GetRouteById(contentId); // cached + if (route != null && route.StartsWith("err/")) route = null; + var domainHelper = new DomainHelper(domainService); IDomain domain; @@ -78,16 +80,14 @@ namespace Umbraco.Web.Models domain = pos == 0 ? null : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; - } + } - if (domain == null || domain.LanguageIsoCode.IsNullOrWhiteSpace()) - return GetDefaultCulture(localizationService); + var rootContentId = domain == null ? -1 : domain.RootContentId; + var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, rootContentId); - var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, domain.RootContentId); - - return wcDomain == null - ? new CultureInfo(domain.LanguageIsoCode) - : new CultureInfo(wcDomain.LanguageIsoCode); + if (wcDomain != null) return new CultureInfo(wcDomain.LanguageIsoCode); + if (domain != null) return new CultureInfo(domain.LanguageIsoCode); + return GetDefaultCulture(localizationService); } private static CultureInfo GetDefaultCulture(ILocalizationService localizationService) diff --git a/src/Umbraco.Web/Models/ImageCropCoordinates.cs b/src/Umbraco.Web/Models/ImageCropCoordinates.cs index 5152ba5b42..4a480b4beb 100644 --- a/src/Umbraco.Web/Models/ImageCropCoordinates.cs +++ b/src/Umbraco.Web/Models/ImageCropCoordinates.cs @@ -1,9 +1,11 @@ +using System; using System.Runtime.Serialization; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropCoordinates")] - public class ImageCropCoordinates + public class ImageCropCoordinates : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "x1")] public decimal X1 { get; set; } @@ -16,5 +18,62 @@ namespace Umbraco.Web.Models [DataMember(Name = "y2")] public decimal Y2 { get; set; } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropCoordinates other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return X1 == other.X1 && Y1 == other.Y1 && X2 == other.X2 && Y2 == other.Y2; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImageCropCoordinates) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = X1.GetHashCode(); + hashCode = (hashCode*397) ^ Y1.GetHashCode(); + hashCode = (hashCode*397) ^ X2.GetHashCode(); + hashCode = (hashCode*397) ^ Y2.GetHashCode(); + return hashCode; + } + } + + public static bool operator ==(ImageCropCoordinates left, ImageCropCoordinates right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropCoordinates left, ImageCropCoordinates right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ImageCropData.cs b/src/Umbraco.Web/Models/ImageCropData.cs index f96e209906..fb1b3c7424 100644 --- a/src/Umbraco.Web/Models/ImageCropData.cs +++ b/src/Umbraco.Web/Models/ImageCropData.cs @@ -1,17 +1,16 @@ -using System.Runtime.Serialization; +using System; +using System.Runtime.Serialization; using System.Threading.Tasks; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropData")] - public class ImageCropData + public class ImageCropData : CaseInsensitiveDynamicObject, IEquatable { [DataMember(Name = "alias")] public string Alias { get; set; } - - //[DataMember(Name = "name")] - //public string Name { get; set; } - + [DataMember(Name = "width")] public int Width { get; set; } @@ -20,6 +19,63 @@ namespace Umbraco.Web.Models [DataMember(Name = "coordinates")] public ImageCropCoordinates Coordinates { get; set; } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropData other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Alias, other.Alias) && Width == other.Width && Height == other.Height && Equals(Coordinates, other.Coordinates); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImageCropData) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = (Alias != null ? Alias.GetHashCode() : 0); + hashCode = (hashCode*397) ^ Width; + hashCode = (hashCode*397) ^ Height; + hashCode = (hashCode*397) ^ (Coordinates != null ? Coordinates.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator ==(ImageCropData left, ImageCropData right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropData left, ImageCropData right) + { + return !Equals(left, right); + } } } diff --git a/src/Umbraco.Web/Models/ImageCropDataSet.cs b/src/Umbraco.Web/Models/ImageCropDataSet.cs index b3b584682a..4c5a68a6b9 100644 --- a/src/Umbraco.Web/Models/ImageCropDataSet.cs +++ b/src/Umbraco.Web/Models/ImageCropDataSet.cs @@ -1,15 +1,27 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Dynamic; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Text; using System.Web; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Dynamics; +using Umbraco.Core.Serialization; +using Umbraco.Web.PropertyEditors.ValueConverters; namespace Umbraco.Web.Models { + [JsonConverter(typeof(NoTypeConverterJsonConverter))] + [TypeConverter(typeof(ImageCropDataSetConverter))] [DataContract(Name="imageCropDataSet")] - public class ImageCropDataSet : IHtmlString - { + public class ImageCropDataSet : CaseInsensitiveDynamicObject, IHtmlString, IEquatable + { + [DataMember(Name="src")] public string Src { get; set;} @@ -19,7 +31,6 @@ namespace Umbraco.Web.Models [DataMember(Name = "crops")] public IEnumerable Crops { get; set; } - public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) { @@ -55,7 +66,7 @@ namespace Umbraco.Web.Models public bool HasFocalPoint() { - return FocalPoint != null && FocalPoint.Top != 0.5m && FocalPoint.Top != 0.5m; + return FocalPoint != null && FocalPoint.Left != 0.5m && FocalPoint.Top != 0.5m; } public bool HasCrop(string alias) @@ -72,5 +83,74 @@ namespace Umbraco.Web.Models { return this.Src; } + + /// + /// Returns a string that represents the current object. + /// + /// + /// If there are crops defined, it will return the JSON value, otherwise it will just return the Src value + /// + public override string ToString() + { + return Crops.Any() ? JsonConvert.SerializeObject(this) : Src; + } + + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropDataSet other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return string.Equals(Src, other.Src) && Equals(FocalPoint, other.FocalPoint) + && Crops.SequenceEqual(other.Crops); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImageCropDataSet) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = (Src != null ? Src.GetHashCode() : 0); + hashCode = (hashCode*397) ^ (FocalPoint != null ? FocalPoint.GetHashCode() : 0); + hashCode = (hashCode*397) ^ (Crops != null ? Crops.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator ==(ImageCropDataSet left, ImageCropDataSet right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropDataSet left, ImageCropDataSet right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ImageCropFocalPoint.cs b/src/Umbraco.Web/Models/ImageCropFocalPoint.cs index e6f12ea81f..e47bab5b7a 100644 --- a/src/Umbraco.Web/Models/ImageCropFocalPoint.cs +++ b/src/Umbraco.Web/Models/ImageCropFocalPoint.cs @@ -1,14 +1,69 @@ +using System; using System.Runtime.Serialization; +using Umbraco.Core.Dynamics; namespace Umbraco.Web.Models { [DataContract(Name = "imageCropFocalPoint")] - public class ImageCropFocalPoint{ - + public class ImageCropFocalPoint : CaseInsensitiveDynamicObject, IEquatable + { [DataMember(Name = "left")] public decimal Left { get; set; } [DataMember(Name = "top")] public decimal Top { get; set; } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(ImageCropFocalPoint other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Left == other.Left && Top == other.Top; + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((ImageCropFocalPoint) obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + unchecked + { + return (Left.GetHashCode()*397) ^ Top.GetHashCode(); + } + } + + public static bool operator ==(ImageCropFocalPoint left, ImageCropFocalPoint right) + { + return Equals(left, right); + } + + public static bool operator !=(ImageCropFocalPoint left, ImageCropFocalPoint right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs b/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs index fd9d1f388f..624c641d3b 100644 --- a/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/AvailablePropertyEditorsResolver.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using AutoMapper; using Umbraco.Core.Models; diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index 0c7dbffd49..efc9b7ced6 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -2,24 +2,20 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Linq.Expressions; -using System.Runtime.Serialization; -using System.Threading; using System.Web; using System.Web.Mvc; using System.Web.Routing; using AutoMapper; -using Newtonsoft.Json; +using umbraco; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Trees; -using umbraco; using Umbraco.Web.Routing; using umbraco.BusinessLogic.Actions; +using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.Models.Mapping { @@ -51,11 +47,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember( dto => dto.IsContainer, expression => expression.MapFrom(content => content.ContentType.IsContainer)) - .ForMember( - dto => dto.IsChildOfListView, - //TODO: Fix this shorthand .Parent() lookup, at least have an overload to use the current - // application context so it's testable! - expression => expression.MapFrom(content => content.Parent().ContentType.IsContainer)) + .ForMember(display => display.IsChildOfListView, expression => expression.Ignore()) .ForMember( dto => dto.Trashed, expression => expression.MapFrom(content => content.Trashed)) @@ -75,10 +67,11 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Notifications, expression => expression.Ignore()) .ForMember(display => display.Errors, expression => expression.Ignore()) .ForMember(display => display.Alias, expression => expression.Ignore()) - .ForMember(display => display.Tabs, expression => expression.ResolveUsing()) + .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.AllowedActions, expression => expression.ResolveUsing( new ActionButtonsResolver(new Lazy(() => applicationContext.Services.UserService)))) - .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService)); + .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, + applicationContext.Services.ContentTypeService)); //FROM IContent TO ContentItemBasic config.CreateMap>() @@ -119,8 +112,37 @@ namespace Umbraco.Web.Models.Mapping /// /// /// - private static void AfterMap(IContent content, ContentItemDisplay display, IDataTypeService dataTypeService, ILocalizedTextService localizedText) + /// + private static void AfterMap(IContent content, ContentItemDisplay display, IDataTypeService dataTypeService, + ILocalizedTextService localizedText, IContentTypeService contentTypeService) { + //map the IsChildOfListView (this is actually if it is a descendant of a list view!) + //TODO: Fix this shorthand .Ancestors() lookup, at least have an overload to use the current + if (content.HasIdentity) + { + var ancesctorListView = content.Ancestors().FirstOrDefault(x => x.ContentType.IsContainer); + display.IsChildOfListView = ancesctorListView != null; + } + else + { + //it's new so it doesn't have a path, so we need to look this up by it's parent + ancestors + var parent = content.Parent(); + if (parent == null) + { + display.IsChildOfListView = false; + } + else if (parent.ContentType.IsContainer) + { + display.IsChildOfListView = true; + } + else + { + var ancesctorListView = parent.Ancestors().FirstOrDefault(x => x.ContentType.IsContainer); + display.IsChildOfListView = ancesctorListView != null; + } + } + + //map the tree node url if (HttpContext.Current != null) { @@ -139,47 +161,90 @@ namespace Umbraco.Web.Models.Mapping if (content.ContentType.IsContainer) { - TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService); + TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService, localizedText); } + + var properties = new List + { + new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/documentType"), + Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), + View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}releasedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/releaseDate"), + Value = display.ReleaseDate.HasValue ? display.ReleaseDate.Value.ToIsoString() : null, + //Not editible for people without publish permission (U4-287) + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + //TODO: Fix up hard coded datepicker + } , + new ContentPropertyDisplay + { + Alias = string.Format("{0}expiredate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/unpublishDate"), + Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, + //Not editible for people without publish permission (U4-287) + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + //TODO: Fix up hard coded datepicker + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}template", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("template/template"), + Value = display.TemplateAlias, + View = "dropdown", //TODO: Hard coding until we make a real dropdown property editor to lookup + Config = new Dictionary + { + {"items", templateItemConfig} + } + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/urls"), + Value = string.Join(",", display.Urls), + View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + } + }; - TabsAndPropertiesResolver.MapGenericProperties( - content, display, - new ContentPropertyDisplay + TabsAndPropertiesResolver.MapGenericProperties(content, display, localizedText, properties.ToArray(), + genericProperties => + { + //TODO: This would be much nicer with the IUmbracoContextAccessor so we don't use singletons + //If this is a web request and there's a user signed in and the + // user has access to the settings section, we will + if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null + && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) { - Alias = string.Format("{0}releasedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/releaseDate"), - Value = display.ReleaseDate.HasValue ? display.ReleaseDate.Value.ToIsoString() : null, - View = "datepicker" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}expiredate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/unpublishDate"), - Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, - View = "datepicker" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}template", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = "Template", //TODO: localize this? - Value = display.TemplateAlias, - View = "dropdown", //TODO: Hard coding until we make a real dropdown property editor to lookup - Config = new Dictionary + var currentDocumentType = contentTypeService.GetContentType(display.ContentTypeAlias); + var currentDocumentTypeName = currentDocumentType == null ? string.Empty : currentDocumentType.Name; + + var currentDocumentTypeId = currentDocumentType == null ? string.Empty : currentDocumentType.Id.ToString(CultureInfo.InvariantCulture); + //TODO: Hard coding this is not good + var docTypeLink = string.Format("#/settings/documenttypes/edit/{0}", currentDocumentTypeId); + + //Replace the doc type property + var docTypeProp = genericProperties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + docTypeProp.Value = new List + { + new { - {"items", templateItemConfig} + linkText = currentDocumentTypeName, + url = docTypeLink, + target = "_self", icon = "icon-item-arrangement" } - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("content/urls"), - Value = string.Join(",", display.Urls), - View = "urllist" //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor - }); + }; + //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor + docTypeProp.View = "urllist"; + } + }); + } - - /// /// Gets the published date value for the IContent object /// @@ -231,35 +296,9 @@ namespace Umbraco.Web.Models.Mapping source.HasIdentity ? source.Id : source.ParentId) .FirstOrDefault(); - if (permissions == null) - { - return Enumerable.Empty(); - } - - var result = new List(); - - //can they publish ? - if (permissions.AssignedPermissions.Contains(ActionPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture))) - { - result.Add(ActionPublish.Instance.Letter); - } - //can they send to publish ? - if (permissions.AssignedPermissions.Contains(ActionToPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture))) - { - result.Add(ActionToPublish.Instance.Letter); - } - //can they save ? - if (permissions.AssignedPermissions.Contains(ActionUpdate.Instance.Letter.ToString(CultureInfo.InvariantCulture))) - { - result.Add(ActionUpdate.Instance.Letter); - } - //can they create ? - if (permissions.AssignedPermissions.Contains(ActionNew.Instance.Letter.ToString(CultureInfo.InvariantCulture))) - { - result.Add(ActionNew.Instance.Letter); - } - - return result; + return permissions == null + ? Enumerable.Empty() + : permissions.AssignedPermissions.Where(x => x.Length == 1).Select(x => x.ToUpperInvariant()[0]); } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index a09592a49d..bf0ec1f457 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -1,8 +1,16 @@ -using AutoMapper; +using System; +using System.Linq; +using System.Threading; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; +using System.Collections.Generic; +using AutoMapper.Internal; +using Umbraco.Core.Services; +using Property = umbraco.NodeFactory.Property; namespace Umbraco.Web.Models.Mapping { @@ -10,12 +18,262 @@ namespace Umbraco.Web.Models.Mapping /// Defines mappings for content/media/members type mappings /// internal class ContentTypeModelMapper : MapperConfiguration - { + { + private readonly Lazy _propertyEditorResolver; + + //default ctor + public ContentTypeModelMapper() + { + _propertyEditorResolver = new Lazy(() => PropertyEditorResolver.Current); + } + + //ctor can be used for testing + public ContentTypeModelMapper(Lazy propertyEditorResolver) + { + _propertyEditorResolver = propertyEditorResolver; + } + public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + + config.CreateMap() + .ConstructUsing(basic => new PropertyType(applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(basic.DataTypeId))) + .ForMember(type => type.ValidationRegExp, expression => expression.ResolveUsing(basic => basic.Validation.Pattern)) + .ForMember(type => type.Mandatory, expression => expression.ResolveUsing(basic => basic.Validation.Mandatory)) + .ForMember(type => type.Name, expression => expression.ResolveUsing(basic => basic.Label)) + .ForMember(type => type.DataTypeDefinitionId, expression => expression.ResolveUsing(basic => basic.DataTypeId)) + .ForMember(type => type.DataTypeId, expression => expression.Ignore()) + .ForMember(type => type.PropertyEditorAlias, expression => expression.Ignore()) + .ForMember(type => type.HelpText, expression => expression.Ignore()) + .ForMember(type => type.Key, expression => expression.Ignore()) + .ForMember(type => type.CreateDate, expression => expression.Ignore()) + .ForMember(type => type.UpdateDate, expression => expression.Ignore()) + .ForMember(type => type.HasIdentity, expression => expression.Ignore()); + + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new ContentType(source.ParentId)) + .ForMember(source => source.AllowedTemplates, expression => expression.Ignore()) + .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) + .AfterMap((source, dest) => + { + dest.AllowedTemplates = source.AllowedTemplates + .Where(x => x != null) + .Select(s => applicationContext.Services.FileService.GetTemplate(s)) + .ToArray(); + + if (source.DefaultTemplate != null) + dest.SetDefaultTemplate(applicationContext.Services.FileService.GetTemplate(source.DefaultTemplate)); + + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + }); + + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new MediaType(source.ParentId)) + .AfterMap((source, dest) => + { + ContentTypeModelMapperExtensions.AfterMapMediaTypeSaveToEntity(source, dest, applicationContext); + }); + + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new MemberType(source.ParentId)) + .AfterMap((source, dest) => + { + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + + //map the MemberCanEditProperty,MemberCanViewProperty + foreach (var propertyType in source.Groups.SelectMany(x => x.Properties)) + { + var localCopy = propertyType; + var destProp = dest.PropertyTypes.SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias)); + if (destProp != null) + { + dest.SetMemberCanEditProperty(localCopy.Alias, localCopy.MemberCanEditProperty); + dest.SetMemberCanViewProperty(localCopy.Alias, localCopy.MemberCanViewProperty); + } + } + }); + + config.CreateMap().ConvertUsing(x => x.Alias); + + config.CreateMap() + //map base logic + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) + .AfterMap((memberType, display) => + { + //map the MemberCanEditProperty,MemberCanViewProperty + foreach (var propertyType in memberType.PropertyTypes) + { + var localCopy = propertyType; + var displayProp = display.Groups.SelectMany(x => x.Properties).SingleOrDefault(x => x.Alias.InvariantEquals(localCopy.Alias)); + if (displayProp != null) + { + displayProp.MemberCanEditProperty = memberType.MemberCanEditProperty(localCopy.Alias); + displayProp.MemberCanViewProperty = memberType.MemberCanViewProperty(localCopy.Alias); + } + } + }); + + config.CreateMap() + //map base logic + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) + .AfterMap((source, dest) => + { + //default listview + dest.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + + if (string.IsNullOrEmpty(source.Name) == false) + { + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; + if (applicationContext.Services.DataTypeService.GetDataTypeDefinitionByName(name) != null) + dest.ListViewEditorName = name; + } + }); + + config.CreateMap() + //map base logic + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) + .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) + .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) + .ForMember(display => display.Notifications, expression => expression.Ignore()) + .AfterMap((source, dest) => + { + //sync templates + dest.AllowedTemplates = source.AllowedTemplates.Select(Mapper.Map).ToArray(); + + if (source.DefaultTemplate != null) + dest.DefaultTemplate = Mapper.Map(source.DefaultTemplate); + + //default listview + dest.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; + + if (string.IsNullOrEmpty(source.Alias) == false) + { + var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Alias; + if (applicationContext.Services.DataTypeService.GetDataTypeDefinitionByName(name) != null) + dest.ListViewEditorName = name; + } + + }); + + config.CreateMap(); config.CreateMap(); config.CreateMap(); - config.CreateMap(); + + config.CreateMap() + + .ConstructUsing(propertyTypeBasic => + { + var dataType = applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(propertyTypeBasic.DataTypeId); + if (dataType == null) throw new NullReferenceException("No data type found with id " + propertyTypeBasic.DataTypeId); + return new PropertyType(dataType, propertyTypeBasic.Alias); + }) + + //only map if it is actually set + .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + //only map if it is actually set, if it's not set, it needs to be handled differently and will be taken care of in the + // IContentType.AddPropertyType + .ForMember(dest => dest.PropertyGroupId, expression => expression.Condition(source => source.GroupId > 0)) + .ForMember(type => type.PropertyGroupId, expression => expression.MapFrom(display => new Lazy(() => display.GroupId, false))) + .ForMember(type => type.Key, expression => expression.Ignore()) + .ForMember(type => type.HelpText, expression => expression.Ignore()) + .ForMember(type => type.HasIdentity, expression => expression.Ignore()) + //ignore because this is set in the ctor NOT ON UPDATE, STUPID! + //.ForMember(type => type.Alias, expression => expression.Ignore()) + //ignore because this is obsolete and shouldn't be used + .ForMember(type => type.DataTypeId, expression => expression.Ignore()) + .ForMember(type => type.Mandatory, expression => expression.MapFrom(display => display.Validation.Mandatory)) + .ForMember(type => type.ValidationRegExp, expression => expression.MapFrom(display => display.Validation.Pattern)) + .ForMember(type => type.DataTypeDefinitionId, expression => expression.MapFrom(display => display.DataTypeId)) + .ForMember(type => type.Name, expression => expression.MapFrom(display => display.Label)); + + #region *** Used for mapping on top of an existing display object from a save object *** + + config.CreateMap() + .MapBaseContentTypeSaveToDisplay(); + + config.CreateMap() + .MapBaseContentTypeSaveToDisplay(); + + config.CreateMap() + .MapBaseContentTypeSaveToDisplay() + .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) + .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) + .AfterMap((source, dest) => + { + //sync templates + var destAllowedTemplateAliases = dest.AllowedTemplates.Select(x => x.Alias); + //if the dest is set and it's the same as the source, then don't change + if (destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) + { + var templates = applicationContext.Services.FileService.GetTemplates(source.AllowedTemplates.ToArray()); + dest.AllowedTemplates = source.AllowedTemplates + .Select(x => Mapper.Map(templates.SingleOrDefault(t => t.Alias == x))) + .WhereNotNull() + .ToArray(); + } + + if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) + { + //if the dest is set and it's the same as the source, then don't change + if (dest.DefaultTemplate == null || source.DefaultTemplate != dest.DefaultTemplate.Alias) + { + var template = applicationContext.Services.FileService.GetTemplate(source.DefaultTemplate); + dest.DefaultTemplate = template == null ? null : Mapper.Map(template); + } + } + else + { + dest.DefaultTemplate = null; + } + }); + + //for doc types, media types + config.CreateMap, PropertyGroup>() + .MapPropertyGroupBasicToPropertyGroupPersistence, PropertyTypeBasic>(); + + //for members + config.CreateMap, PropertyGroup>() + .MapPropertyGroupBasicToPropertyGroupPersistence, MemberPropertyTypeBasic>(); + + //for doc types, media types + config.CreateMap, PropertyGroupDisplay>() + .MapPropertyGroupBasicToPropertyGroupDisplay, PropertyTypeBasic, PropertyTypeDisplay>(); + + //for members + config.CreateMap, PropertyGroupDisplay>() + .MapPropertyGroupBasicToPropertyGroupDisplay, MemberPropertyTypeBasic, MemberPropertyTypeDisplay>(); + + config.CreateMap() + .ForMember(g => g.Editor, expression => expression.Ignore()) + .ForMember(g => g.View, expression => expression.Ignore()) + .ForMember(g => g.Config, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeName, expression => expression.Ignore()) + .ForMember(g => g.Locked, exp => exp.Ignore()); + + config.CreateMap() + .ForMember(g => g.Editor, expression => expression.Ignore()) + .ForMember(g => g.View, expression => expression.Ignore()) + .ForMember(g => g.Config, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeName, expression => expression.Ignore()) + .ForMember(g => g.Locked, exp => exp.Ignore()); + + #endregion + + + + } + + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs new file mode 100644 index 0000000000..52f2dbad4b --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -0,0 +1,337 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Used as a shared way to do the underlying mapping for content types base classes + /// + /// + /// We used to use 'Include' Automapper inheritance functionality and although this works, the unit test + /// to assert mappings fails which is an Automapper bug. So instead we will use an extension method for the mappings + /// to re-use mappings. + /// + internal static class ContentTypeModelMapperExtensions + { + + public static IMappingExpression MapPropertyGroupBasicToPropertyGroupPersistence( + this IMappingExpression mapping) + where TSource : PropertyGroupBasic + where TPropertyTypeBasic : PropertyTypeBasic + { + return mapping + .ForMember(dest => dest.Id, map => map.Condition(source => source.Id > 0)) + .ForMember(dest => dest.Key, map => map.Ignore()) + .ForMember(dest => dest.HasIdentity, map => map.Ignore()) + .ForMember(dest => dest.CreateDate, map => map.Ignore()) + .ForMember(dest => dest.UpdateDate, map => map.Ignore()) + .ForMember(dest => dest.PropertyTypes, map => map.Ignore()); + } + + public static IMappingExpression> MapPropertyGroupBasicToPropertyGroupDisplay( + this IMappingExpression> mapping) + where TSource : PropertyGroupBasic + where TPropertyTypeBasic : PropertyTypeBasic + where TPropertyTypeDisplay : PropertyTypeDisplay + { + return mapping + .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.ParentTabContentTypes, expression => expression.Ignore()) + .ForMember(g => g.ParentTabContentTypeNames, expression => expression.Ignore()) + .ForMember(g => g.Properties, expression => expression.MapFrom(display => display.Properties.Select(Mapper.Map))); + } + + public static void AfterMapContentTypeSaveToEntity( + TSource source, TDestination dest, + ApplicationContext applicationContext) + where TSource : ContentTypeSave + where TDestination : IContentTypeComposition + { + //sync compositions + var current = dest.CompositionAliases().ToArray(); + var proposed = source.CompositeContentTypes; + + var remove = current.Where(x => proposed.Contains(x) == false); + var add = proposed.Where(x => current.Contains(x) == false); + + foreach (var rem in remove) + { + dest.RemoveContentType(rem); + } + + foreach (var a in add) + { + //TODO: Remove N+1 lookup + var addCt = applicationContext.Services.ContentTypeService.GetContentType(a); + if (addCt != null) + dest.AddContentType(addCt); + } + } + + public static void AfterMapMediaTypeSaveToEntity( + TSource source, TDestination dest, + ApplicationContext applicationContext) + where TSource : MediaTypeSave + where TDestination : IContentTypeComposition + { + //sync compositions + var current = dest.CompositionAliases().ToArray(); + var proposed = source.CompositeContentTypes; + + var remove = current.Where(x => proposed.Contains(x) == false); + var add = proposed.Where(x => current.Contains(x) == false); + + foreach (var rem in remove) + { + dest.RemoveContentType(rem); + } + + foreach (var a in add) + { + //TODO: Remove N+1 lookup + var addCt = applicationContext.Services.ContentTypeService.GetMediaType(a); + if (addCt != null) + dest.AddContentType(addCt); + } + } + + public static IMappingExpression MapBaseContentTypeSaveToDisplay( + this IMappingExpression mapping) + where TSource : ContentTypeSave + where TDestination : ContentTypeCompositionDisplay + where TPropertyTypeDestination : PropertyTypeDisplay + where TPropertyTypeSource : PropertyTypeBasic + { + return mapping + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore()) + .ForMember(dto => dto.Notifications, expression => expression.Ignore()) + .ForMember(dto => dto.Errors, expression => expression.Ignore()) + .ForMember(dto => dto.LockedCompositeContentTypes, exp => exp.Ignore()) + .ForMember(dto => dto.Groups, expression => expression.ResolveUsing(new PropertyGroupDisplayResolver())); + } + + public static IMappingExpression MapBaseContentTypeEntityToDisplay( + this IMappingExpression mapping, ApplicationContext applicationContext, Lazy propertyEditorResolver) + where TSource : IContentTypeComposition + where TDestination : ContentTypeCompositionDisplay + where TPropertyTypeDisplay : PropertyTypeDisplay, new() + { + return mapping + .ForMember(display => display.Notifications, expression => expression.Ignore()) + .ForMember(display => display.Errors, expression => expression.Ignore()) + .ForMember(display => display.AllowAsRoot, expression => expression.MapFrom(type => type.AllowedAsRoot)) + .ForMember(display => display.ListViewEditorName, expression => expression.Ignore()) + //Ignore because this is not actually used for content types + .ForMember(display => display.Trashed, expression => expression.Ignore()) + + .ForMember( + dto => dto.AllowedContentTypes, + expression => expression.MapFrom(dto => dto.AllowedContentTypes.Select(x => x.Id.Value))) + + .ForMember( + dto => dto.CompositeContentTypes, + expression => expression.MapFrom(dto => dto.ContentTypeComposition)) + + .ForMember( + dto => dto.LockedCompositeContentTypes, + expression => expression.ResolveUsing(new LockedCompositionsResolver(applicationContext))) + + .ForMember( + dto => dto.Groups, + expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, propertyEditorResolver))); + } + + /// + /// Display -> Entity class base mapping logic + /// + /// + /// + /// + /// + /// + /// + public static IMappingExpression MapBaseContentTypeSaveToEntity( + this IMappingExpression mapping, ApplicationContext applicationContext) + //where TSource : ContentTypeCompositionDisplay + where TSource : ContentTypeSave + where TDestination : IContentTypeComposition + where TSourcePropertyType : PropertyTypeBasic + { + return mapping + //only map id if set to something higher then zero + .ForMember(dto => dto.Id, expression => expression.Condition(display => (Convert.ToInt32(display.Id) > 0))) + .ForMember(dto => dto.Id, expression => expression.MapFrom(display => Convert.ToInt32(display.Id))) + + //These get persisted as part of the saving procedure, nothing to do with the display model + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + + .ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot)) + .ForMember(dto => dto.CreatorId, expression => expression.Ignore()) + .ForMember(dto => dto.Level, expression => expression.Ignore()) + .ForMember(dto => dto.SortOrder, expression => expression.Ignore()) + //ignore, we'll do this in after map + .ForMember(dto => dto.PropertyGroups, expression => expression.Ignore()) + .ForMember(dto => dto.NoGroupPropertyTypes, expression => expression.Ignore()) + // ignore, composition is managed in AfterMapContentTypeSaveToEntity + .ForMember(dest => dest.ContentTypeComposition, opt => opt.Ignore()) + + .ForMember( + dto => dto.AllowedContentTypes, + expression => expression.MapFrom(dto => dto.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)))) + + .AfterMap((source, dest) => + { + // handle property groups and property types + // note that ContentTypeSave has + // - all groups, inherited and local; only *one* occurence per group *name* + // - potentially including the generic properties group + // - all properties, inherited and local + // + // also, see PropertyTypeGroupResolver.ResolveCore: + // - if a group is local *and* inherited, then Inherited is true + // and the identifier is the identifier of the *local* group + // + // IContentTypeComposition AddPropertyGroup, AddPropertyType methods do some + // unique-alias-checking, etc that is *not* compatible with re-mapping everything + // the way we do it here, so we should exclusively do it by + // - managing a property group's PropertyTypes collection + // - managing the content type's PropertyTypes collection (for generic properties) + + // handle actual groups (non-generic-properties) + var destOrigGroups = dest.PropertyGroups.ToArray(); // local groups + var destOrigProperties = dest.PropertyTypes.ToArray(); // all properties, in groups or not + var destGroups = new List(); + var sourceGroups = source.Groups.Where(x => x.IsGenericProperties == false).ToArray(); + foreach (var sourceGroup in sourceGroups) + { + // get the dest group + var destGroup = MapSaveGroup(sourceGroup, destOrigGroups); + + // handle local properties + var destProperties = sourceGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties)) + .ToArray(); + + // if the group has no local properties, skip it, ie sort-of garbage-collect + // local groups which would not have local properties anymore + if (destProperties.Length == 0) + continue; + + // ensure no duplicate alias, then assign the group properties collection + EnsureUniqueAliases(destProperties); + destGroup.PropertyTypes = new PropertyTypeCollection(destProperties); + destGroups.Add(destGroup); + } + + // ensure no duplicate name, then assign the groups collection + EnsureUniqueNames(destGroups); + dest.PropertyGroups = new PropertyGroupCollection(destGroups); + + // because the property groups collection was rebuilt, there is no need to remove + // the old groups - they are just gone and will be cleared by the repository + + // handle non-grouped (ie generic) properties + var genericPropertiesGroup = source.Groups.FirstOrDefault(x => x.IsGenericProperties); + if (genericPropertiesGroup != null) + { + // handle local properties + var destProperties = genericPropertiesGroup.Properties + .Where(x => x.Inherited == false) + .Select(x => MapSaveProperty(x, destOrigProperties)) + .ToArray(); + + // ensure no duplicate alias, then assign the generic properties collection + EnsureUniqueAliases(destProperties); + dest.NoGroupPropertyTypes = new PropertyTypeCollection(destProperties); + } + + // because all property collections were rebuilt, there is no need to remove + // some old properties, they are just gone and will be cleared by the repository + }); + } + + private static PropertyGroup MapSaveGroup(PropertyGroupBasic sourceGroup, IEnumerable destOrigGroups) + where TPropertyType: PropertyTypeBasic + { + PropertyGroup destGroup; + if (sourceGroup.Id > 0) + { + // update an existing group + // ensure it is still there, then map/update + destGroup = destOrigGroups.FirstOrDefault(x => x.Id == sourceGroup.Id); + if (destGroup != null) + { + Mapper.Map(sourceGroup, destGroup); + return destGroup; + } + + // force-clear the ID as it does not match anything + sourceGroup.Id = 0; + } + + // insert a new group, or update an existing group that has + // been deleted in the meantime and we need to re-create + // map/create + destGroup = Mapper.Map(sourceGroup); + return destGroup; + } + + private static PropertyType MapSaveProperty(PropertyTypeBasic sourceProperty, IEnumerable destOrigProperties) + { + PropertyType destProperty; + if (sourceProperty.Id > 0) + { + // updateg an existing property + // ensure it is still there, then map/update + destProperty = destOrigProperties.FirstOrDefault(x => x.Id == sourceProperty.Id); + if (destProperty != null) + { + Mapper.Map(sourceProperty, destProperty); + return destProperty; + } + + // force-clear the ID as it does not match anything + sourceProperty.Id = 0; + } + + // insert a new property, or update an existing property that has + // been deletedin the meantime and we need to re-create + // map/create + destProperty = Mapper.Map(sourceProperty); + return destProperty; + } + + private static void EnsureUniqueAliases(IEnumerable properties) + { + var propertiesA = properties.ToArray(); + var distinctProperties = propertiesA + .Select(x => x.Alias.ToUpperInvariant()) + .Distinct() + .Count(); + if (distinctProperties != propertiesA.Length) + throw new InvalidOperationException("Cannot map properties due to alias conflict."); + } + + private static void EnsureUniqueNames(IEnumerable groups) + { + var groupsA = groups.ToArray(); + var distinctProperties = groupsA + .Select(x => x.Name.ToUpperInvariant()) + .Distinct() + .Count(); + if (distinctProperties != groupsA.Length) + throw new InvalidOperationException("Cannot map groups due to name conflict."); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index 8d0411dd1e..70ff63ca12 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using AutoMapper; -using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; @@ -34,10 +33,32 @@ namespace Umbraco.Web.Models.Mapping Constants.System.DefaultMembersListViewDataTypeId }; - config.CreateMap() + config.CreateMap() + .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) + .ForMember(x => x.IsSystemDataType, expression => expression.Ignore()) + .ForMember(x => x.Id, expression => expression.Ignore()) + .ForMember(x => x.Trashed, expression => expression.Ignore()) + .ForMember(x => x.Key, expression => expression.Ignore()) + .ForMember(x => x.ParentId, expression => expression.Ignore()) + .ForMember(x => x.Path, expression => expression.Ignore()) + .ForMember(x => x.AdditionalData, expression => expression.Ignore()); + + config.CreateMap() + .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(x => x.IsSystemDataType, expression => expression.MapFrom(definition => systemIds.Contains(definition.Id))); + .ForMember(x => x.Group, expression => expression.Ignore()) + .ForMember(x => x.IsSystemDataType, expression => expression.MapFrom(definition => systemIds.Contains(definition.Id))) + .AfterMap((def, basic) => + { + var editor = PropertyEditorResolver.Current.GetByAlias(def.PropertyEditorAlias); + if (editor != null) + { + basic.Alias = editor.Alias; + basic.Group = editor.Group; + basic.Icon = editor.Icon; + } + }); config.CreateMap() .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing()) @@ -45,10 +66,21 @@ namespace Umbraco.Web.Models.Mapping new PreValueDisplayResolver(lazyDataTypeService))) .ForMember(display => display.SelectedEditor, expression => expression.MapFrom( definition => definition.PropertyEditorAlias.IsNullOrWhiteSpace() ? null : definition.PropertyEditorAlias)) + .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.Notifications, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(x => x.IsSystemDataType, expression => expression.MapFrom(definition => systemIds.Contains(definition.Id))); + .ForMember(x => x.Group, expression => expression.Ignore()) + .ForMember(x => x.IsSystemDataType, expression => expression.MapFrom(definition => systemIds.Contains(definition.Id))) + .AfterMap((def, basic) => + { + var editor = PropertyEditorResolver.Current.GetByAlias(def.PropertyEditorAlias); + if (editor != null) + { + basic.Group = editor.Group; + basic.Icon = editor.Icon; + } + }); //gets a list of PreValueFieldDisplay objects from the data type definition config.CreateMap>() @@ -66,7 +98,6 @@ namespace Umbraco.Web.Models.Mapping .ForMember(definition => definition.Key, expression => expression.Ignore()) .ForMember(definition => definition.Path, expression => expression.Ignore()) .ForMember(definition => definition.PropertyEditorAlias, expression => expression.MapFrom(save => save.SelectedEditor)) - .ForMember(definition => definition.ParentId, expression => expression.MapFrom(save => -1)) .ForMember(definition => definition.DatabaseType, expression => expression.ResolveUsing()) .ForMember(x => x.ControlId, expression => expression.Ignore()) .ForMember(x => x.CreatorId, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index fb0a809c24..5610a70008 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -45,6 +45,36 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.Trashed, expression => expression.Ignore()) .ForMember(x => x.AdditionalData, expression => expression.Ignore()); + config.CreateMap() + .ForMember(basic => basic.Icon, expression => expression.UseValue("icon-layout")) + .ForMember(basic => basic.Path, expression => expression.MapFrom(template => template.Path)) + .ForMember(basic => basic.ParentId, expression => expression.UseValue(-1)) + .ForMember(dto => dto.Trashed, expression => expression.Ignore()) + .ForMember(x => x.AdditionalData, expression => expression.Ignore()); + + //config.CreateMap() + // .ConstructUsing(basic => new Template(basic.Name, basic.Alias) + // { + // Id = Convert.ToInt32(basic.Id), + // Key = basic.Key + // }) + // .ForMember(t => t.Path, expression => expression.Ignore()) + // .ForMember(t => t.Id, expression => expression.MapFrom(template => Convert.ToInt32(template.Id))) + // .ForMember(x => x.VirtualPath, expression => expression.Ignore()) + // .ForMember(x => x.CreateDate, expression => expression.Ignore()) + // .ForMember(x => x.UpdateDate, expression => expression.Ignore()) + // .ForMember(x => x.Content, expression => expression.Ignore()); + + config.CreateMap() + .ForMember(x => x.Id, expression => expression.MapFrom(entity => new Lazy(() => Convert.ToInt32(entity.Id)))) + .ForMember(x => x.SortOrder, expression => expression.Ignore()); + + config.CreateMap() + .ForMember(basic => basic.Path, expression => expression.MapFrom(x => x.Path)) + .ForMember(basic => basic.ParentId, expression => expression.MapFrom(x => x.ParentId)) + .ForMember(dto => dto.Trashed, expression => expression.Ignore()) + .ForMember(x => x.AdditionalData, expression => expression.Ignore()); + config.CreateMap() //default to document icon .ForMember(x => x.Icon, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs new file mode 100644 index 0000000000..d3d692d10d --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs @@ -0,0 +1,43 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.Mapping +{ + internal class LockedCompositionsResolver : ValueResolver> + { + private readonly ApplicationContext _applicationContext; + + public LockedCompositionsResolver(ApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + protected override IEnumerable ResolveCore(IContentTypeComposition source) + { + var aliases = new List(); + // get ancestor ids from path of parent if not root + if (source.ParentId != Constants.System.Root) + { + var parent = _applicationContext.Services.ContentTypeService.GetContentType(source.ParentId); + if (parent != null) + { + var ancestorIds = parent.Path.Split(',').Select(int.Parse); + // loop through all content types and return ordered aliases of ancestors + var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); + foreach (var ancestorId in ancestorIds) + { + var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor != null) + { + aliases.Add(ancestor.Alias); + } + } + } + } + return aliases.OrderBy(x => x); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 241d5b3c97..799a93a220 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -7,7 +7,10 @@ using System.Web; using System.Web.Mvc; using System.Web.Routing; using AutoMapper; +using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; using Umbraco.Core.PropertyEditors; @@ -35,11 +38,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember( dto => dto.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) - .ForMember( - dto => dto.IsChildOfListView, - //TODO: Fix this shorthand .Parent() lookup, at least have an overload to use the current - // application context so it's testable! - expression => expression.MapFrom(content => content.Parent().ContentType.IsContainer)) + .ForMember(display => display.IsChildOfListView, expression => expression.Ignore()) .ForMember( dto => dto.Trashed, expression => expression.MapFrom(content => content.Trashed)) @@ -54,8 +53,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Updater, expression => expression.Ignore()) .ForMember(display => display.Alias, expression => expression.Ignore()) .ForMember(display => display.IsContainer, expression => expression.Ignore()) - .ForMember(display => display.Tabs, expression => expression.ResolveUsing()) - .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService)); + .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) + .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, applicationContext.ProfilingLogger.Logger)); //FROM IMedia TO ContentItemBasic config.CreateMap>() @@ -86,8 +85,35 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.Alias, expression => expression.Ignore()); } - private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeService dataTypeService) + private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeService dataTypeService, ILocalizedTextService localizedText, ILogger logger) { + // Adapted from ContentModelMapper + //map the IsChildOfListView (this is actually if it is a descendant of a list view!) + //TODO: Fix this shorthand .Ancestors() lookup, at least have an overload to use the current + if (media.HasIdentity) + { + var ancesctorListView = media.Ancestors().FirstOrDefault(x => x.ContentType.IsContainer); + display.IsChildOfListView = ancesctorListView != null; + } + else + { + //it's new so it doesn't have a path, so we need to look this up by it's parent + ancestors + var parent = media.Parent(); + if (parent == null) + { + display.IsChildOfListView = false; + } + else if (parent.ContentType.IsContainer) + { + display.IsChildOfListView = true; + } + else + { + var ancesctorListView = parent.Ancestors().FirstOrDefault(x => x.ContentType.IsContainer); + display.IsChildOfListView = ancesctorListView != null; + } + } + //map the tree node url if (HttpContext.Current != null) { @@ -98,10 +124,35 @@ namespace Umbraco.Web.Models.Mapping if (media.ContentType.IsContainer) { - TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService); + TabsAndPropertiesResolver.AddListView(display, "media", dataTypeService, localizedText); + } + + var genericProperties = new List + { + new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/mediatype"), + Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), + View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + } + }; + + var links = media.GetUrls(UmbracoConfig.For.UmbracoSettings().Content, logger); + + if (links.Any()) + { + var link = new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("media/urls"), + Value = string.Join(",", links), + View = "urllist" + }; + genericProperties.Add(link); } - TabsAndPropertiesResolver.MapGenericProperties(media, display); + TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties); } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index d2558d0451..52fadd7a6a 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using umbraco; using System.Linq; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Web.Trees; @@ -75,7 +76,7 @@ namespace Umbraco.Web.Models.Mapping expression => expression.MapFrom(content => content.ContentType.Name)) .ForMember(display => display.Properties, expression => expression.Ignore()) .ForMember(display => display.Tabs, - expression => expression.ResolveUsing()) + expression => expression.ResolveUsing(new MemberTabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.MemberProviderFieldMapping, expression => expression.ResolveUsing()) .ForMember(display => display.MembershipScenario, @@ -89,7 +90,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(display => display.Trashed, expression => expression.Ignore()) .ForMember(display => display.IsContainer, expression => expression.Ignore()) .ForMember(display => display.TreeNodeUrl, expression => expression.Ignore()) - .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, member, display)); + .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, member, display, applicationContext.Services.TextService)); //FROM IMember TO MemberBasic config.CreateMap() @@ -163,10 +164,11 @@ namespace Umbraco.Web.Models.Mapping /// /// /// + /// /// /// If this is a new entity and there is an approved field then we'll set it to true by default. /// - private static void MapGenericCustomProperties(IMemberService memberService, IMember member, MemberDisplay display) + private static void MapGenericCustomProperties(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) { var membersProvider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); @@ -177,47 +179,58 @@ namespace Umbraco.Web.Models.Mapping var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Key.ToString("N"), null)); display.TreeNodeUrl = url; } - - TabsAndPropertiesResolver.MapGenericProperties( - member, display, - GetLoginProperty(memberService, member, display), + + var genericProperties = new List + { new ContentPropertyDisplay + { + Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/membertype"), + Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), + View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View + }, + GetLoginProperty(memberService, member, display, localizedText), + new ContentPropertyDisplay + { + Alias = string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("general/email"), + Value = display.Email, + View = "email", + Validation = {Mandatory = true} + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("password"), + //NOTE: The value here is a json value - but the only property we care about is the generatedPassword one if it exists, the newPassword exists + // only when creating a new member and we want to have a generated password pre-filled. + Value = new Dictionary { - Alias = string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("general", "email"), - Value = display.Email, - View = "email", - Validation = { Mandatory = true } + {"generatedPassword", member.GetAdditionalDataValueIgnoreCase("GeneratedPassword", null)}, + {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, }, - new ContentPropertyDisplay + //TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor + View = "changepassword", + //initialize the dictionary with the configuration from the default membership provider + Config = new Dictionary(membersProvider.GetConfiguration()) { - Alias = string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("password"), - //NOTE: The value here is a json value - but the only property we care about is the generatedPassword one if it exists, the newPassword exists - // only when creating a new member and we want to have a generated password pre-filled. - Value = new Dictionary - { - {"generatedPassword", member.GetAdditionalDataValueIgnoreCase("GeneratedPassword", null) }, - {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) }, - }, - //TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor - View = "changepassword", - //initialize the dictionary with the configuration from the default membership provider - Config = new Dictionary(membersProvider.GetConfiguration()) - { - //the password change toggle will only be displayed if there is already a password assigned. - {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} - } - }, + //the password change toggle will only be displayed if there is already a password assigned. + {"hasPassword", member.RawPasswordValue.IsNullOrWhiteSpace() == false} + } + }, new ContentPropertyDisplay - { - Alias = string.Format("{0}membergroup", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "membergroup"), - Value = GetMemberGroupValue(display.Username), - View = "membergroups", - Config = new Dictionary { { "IsRequired", true } } - }); + { + Alias = string.Format("{0}membergroup", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("content/membergroup"), + Value = GetMemberGroupValue(display.Username), + View = "membergroups", + Config = new Dictionary {{"IsRequired", true}} + } + }; + + TabsAndPropertiesResolver.MapGenericProperties(member, display, localizedText, genericProperties); + //check if there's an approval field var provider = membersProvider as global::umbraco.providers.members.UmbracoMembershipProvider; if (member.HasIdentity == false && provider != null) @@ -244,12 +257,12 @@ namespace Umbraco.Web.Models.Mapping /// the membership provider is a custom one, we cannot allow chaning the username because MembershipProvider's do not actually natively /// allow that. /// - internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, MemberDisplay display) + internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) { var prop = new ContentPropertyDisplay { Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("login"), + Label = localizedText.Localize("login"), Value = display.Username }; @@ -318,6 +331,20 @@ namespace Umbraco.Web.Models.Mapping /// internal class MemberTabsAndPropertiesResolver : TabsAndPropertiesResolver { + private readonly ILocalizedTextService _localizedTextService; + + public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService) + : base(localizedTextService) + { + _localizedTextService = localizedTextService; + } + + public MemberTabsAndPropertiesResolver(ILocalizedTextService localizedTextService, + IEnumerable ignoreProperties) : base(localizedTextService, ignoreProperties) + { + _localizedTextService = localizedTextService; + } + protected override IEnumerable> ResolveCore(IContentBase content) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); @@ -336,7 +363,7 @@ namespace Umbraco.Web.Models.Mapping if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = ui.Text("general", "no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } return result; @@ -352,7 +379,7 @@ namespace Umbraco.Web.Models.Mapping if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") { isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = ui.Text("general", "no"); + isLockedOutProperty.Value = _localizedTextService.Localize("general/no"); } return result; diff --git a/src/Umbraco.Web/Models/Mapping/PropertyGroupDisplayResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyGroupDisplayResolver.cs new file mode 100644 index 0000000000..2f11eb04c4 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/PropertyGroupDisplayResolver.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class PropertyGroupDisplayResolver : ValueResolver>> + where TSource : ContentTypeSave + where TPropertyTypeDestination : PropertyTypeDisplay + where TPropertyTypeSource : PropertyTypeBasic + { + protected override IEnumerable> ResolveCore(TSource source) + { + return source.Groups.Select(Mapper.Map>); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs new file mode 100644 index 0000000000..a2273b07b2 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs @@ -0,0 +1,229 @@ +using AutoMapper; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + internal class PropertyTypeGroupResolver : ValueResolver>> + where TPropertyType : PropertyTypeDisplay, new() + { + private readonly ApplicationContext _applicationContext; + private readonly Lazy _propertyEditorResolver; + + public PropertyTypeGroupResolver(ApplicationContext applicationContext, Lazy propertyEditorResolver) + { + _applicationContext = applicationContext; + _propertyEditorResolver = propertyEditorResolver; + } + + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property group. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition GetContentTypeForPropertyGroup(IContentTypeComposition contentType, int propertyGroupId) + { + // test local groups + if (contentType.PropertyGroups.Any(x => x.Id == propertyGroupId)) + return contentType; + + // test composition types groups + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyGroup(x, propertyGroupId)) + .FirstOrDefault(x => x != null); + } + + /// + /// Gets the content type that defines a property group, within a composition. + /// + /// The composition. + /// The identifier of the property type. + /// The composition content type that defines the specified property group. + private static IContentTypeComposition GetContentTypeForPropertyType(IContentTypeComposition contentType, int propertyTypeId) + { + // test local property types + if (contentType.PropertyTypes.Any(x => x.Id == propertyTypeId)) + return contentType; + + // test composition property types + // .ContentTypeComposition is just the local ones, not recursive, + // so we have to recurse here + return contentType.ContentTypeComposition + .Select(x => GetContentTypeForPropertyType(x, propertyTypeId)) + .FirstOrDefault(x => x != null); + } + + protected override IEnumerable> ResolveCore(IContentTypeComposition source) + { + // deal with groups + var groups = new List>(); + + // add groups local to this content type + foreach (var tab in source.PropertyGroups) + { + var group = new PropertyGroupDisplay + { + Id = tab.Id, + Inherited = false, + Name = tab.Name, + SortOrder = tab.SortOrder, + ContentTypeId = source.Id + }; + + group.Properties = MapProperties(tab.PropertyTypes, source, tab.Id, false); + groups.Add(group); + } + + // add groups inherited through composition + var localGroupIds = groups.Select(x => x.Id).ToArray(); + foreach (var tab in source.CompositionPropertyGroups) + { + // skip those that are local to this content type + if (localGroupIds.Contains(tab.Id)) continue; + + // get the content type that defines this group + var definingContentType = GetContentTypeForPropertyGroup(source, tab.Id); + if (definingContentType == null) + throw new Exception("PropertyGroup with id=" + tab.Id + " was not found on any of the content type's compositions."); + + var group = new PropertyGroupDisplay + { + Id = tab.Id, + Inherited = true, + Name = tab.Name, + SortOrder = tab.SortOrder, + ContentTypeId = definingContentType.Id, + ParentTabContentTypes = new[] { definingContentType.Id }, + ParentTabContentTypeNames = new[] { definingContentType.Name } + }; + + group.Properties = MapProperties(tab.PropertyTypes, definingContentType, tab.Id, true); + groups.Add(group); + } + + // deal with generic properties + var genericProperties = new List(); + + // add generic properties local to this content type + var entityGenericProperties = source.PropertyTypes.Where(x => x.PropertyGroupId == null); + genericProperties.AddRange(MapProperties(entityGenericProperties, source, PropertyGroupBasic.GenericPropertiesGroupId, false)); + + // add generic properties inherited through compositions + var localGenericPropertyIds = genericProperties.Select(x => x.Id).ToArray(); + var compositionGenericProperties = source.CompositionPropertyTypes + .Where(x => x.PropertyGroupId == null // generic + && localGenericPropertyIds.Contains(x.Id) == false); // skip those that are local + foreach (var compositionGenericProperty in compositionGenericProperties) + { + var definingContentType = GetContentTypeForPropertyType(source, compositionGenericProperty.Id); + if (definingContentType == null) + throw new Exception("PropertyType with id=" + compositionGenericProperty.Id + " was not found on any of the content type's compositions."); + genericProperties.AddRange(MapProperties(new [] { compositionGenericProperty }, definingContentType, PropertyGroupBasic.GenericPropertiesGroupId, true)); + } + + // if there are any generic properties, add the corresponding tab + if (genericProperties.Any()) + { + var genericTab = new PropertyGroupDisplay + { + Id = PropertyGroupBasic.GenericPropertiesGroupId, + Name = "Generic properties", + ContentTypeId = source.Id, + SortOrder = 999, + Inherited = false, + Properties = genericProperties + }; + groups.Add(genericTab); + } + + // handle locked properties + var lockedPropertyAliases = new List(); + // add built-in member property aliases to list of aliases to be locked + foreach (var propertyAlias in Constants.Conventions.Member.GetStandardPropertyTypeStubs().Keys) + { + lockedPropertyAliases.Add(propertyAlias); + } + // lock properties by aliases + foreach (var property in groups.SelectMany(x => x.Properties)) + { + property.Locked = lockedPropertyAliases.Contains(property.Alias); + } + + // now merge tabs based on names + // as for one name, we might have one local tab, plus some inherited tabs + var groupsGroupsByName = groups.GroupBy(x => x.Name).ToArray(); + groups = new List>(); // start with a fresh list + foreach (var groupsByName in groupsGroupsByName) + { + // single group, just use it + if (groupsByName.Count() == 1) + { + groups.Add(groupsByName.First()); + continue; + } + + // multiple groups, merge + var group = groupsByName.FirstOrDefault(x => x.Inherited == false) // try local + ?? groupsByName.First(); // else pick one randomly + groups.Add(group); + + // in case we use the local one, flag as inherited + group.Inherited = true; + + // merge (and sort) properties + var properties = groupsByName.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray(); + group.Properties = properties; + + // collect parent group info + var parentGroups = groupsByName.Where(x => x.ContentTypeId != source.Id).ToArray(); + group.ParentTabContentTypes = parentGroups.SelectMany(x => x.ParentTabContentTypes).ToArray(); + group.ParentTabContentTypeNames = parentGroups.SelectMany(x => x.ParentTabContentTypeNames).ToArray(); + } + + return groups.OrderBy(x => x.SortOrder); + } + + private IEnumerable MapProperties(IEnumerable properties, IContentTypeBase contentType, int groupId, bool inherited) + { + var mappedProperties = new List(); + + foreach (var p in properties.Where(x => x.DataTypeDefinitionId != 0).OrderBy(x => x.SortOrder)) + { + var propertyEditor = _propertyEditorResolver.Value.GetByAlias(p.PropertyEditorAlias); + var preValues = _applicationContext.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(p.DataTypeDefinitionId); + + if (propertyEditor == null) + throw new InvalidOperationException("No property editor could be resolved with the alias: " + p.PropertyEditorAlias + ", ensure all packages are installed correctly."); + + mappedProperties.Add(new TPropertyType + { + Id = p.Id, + Alias = p.Alias, + Description = p.Description, + Editor = p.PropertyEditorAlias, + Validation = new PropertyTypeValidation {Mandatory = p.Mandatory, Pattern = p.ValidationRegExp}, + Label = p.Name, + View = propertyEditor.ValueEditor.View, + Config = propertyEditor.PreValueEditor.ConvertDbToEditor(propertyEditor.DefaultPreValues, preValues), + //Value = "", + GroupId = groupId, + Inherited = inherited, + DataTypeId = p.DataTypeDefinitionId, + SortOrder = p.SortOrder, + ContentTypeId = contentType.Id, + ContentTypeName = contentType.Name + }); + } + + return mappedProperties; + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 325b8df3e3..ff4176a944 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; using Umbraco.Core; -using Umbraco.Core.Dictionary; -using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -18,16 +16,19 @@ namespace Umbraco.Web.Models.Mapping /// internal class TabsAndPropertiesResolver : ValueResolver>> { - private ICultureDictionary _cultureDictionary; + private readonly ILocalizedTextService _localizedTextService; protected IEnumerable IgnoreProperties { get; set; } - public TabsAndPropertiesResolver() + public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService) { + if (localizedTextService == null) throw new ArgumentNullException("localizedTextService"); + _localizedTextService = localizedTextService; IgnoreProperties = new List(); } - public TabsAndPropertiesResolver(IEnumerable ignoreProperties) - { + public TabsAndPropertiesResolver(ILocalizedTextService localizedTextService, IEnumerable ignoreProperties) + : this(localizedTextService) + { if (ignoreProperties == null) throw new ArgumentNullException("ignoreProperties"); IgnoreProperties = ignoreProperties; } @@ -37,9 +38,11 @@ namespace Umbraco.Web.Models.Mapping /// /// /// + /// /// /// Any additional custom properties to assign to the generic properties tab. /// + /// /// /// The generic properties tab is mapped during AfterMap and is responsible for /// setting up the properties such as Created date, updated date, template selected, etc... @@ -47,11 +50,11 @@ namespace Umbraco.Web.Models.Mapping public static void MapGenericProperties( TPersisted content, ContentItemDisplayBase display, - params ContentPropertyDisplay[] customProperties) + ILocalizedTextService localizedTextService, + IEnumerable customProperties = null, + Action> onGenericPropertiesMapped = null) where TPersisted : IContentBase { - - var genericProps = display.Tabs.Single(x => x.Id == 0); //store the current props to append to the newly inserted ones @@ -60,56 +63,57 @@ namespace Umbraco.Web.Models.Mapping var labelEditor = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View; var contentProps = new List + { + new ContentPropertyDisplay { - new ContentPropertyDisplay - { - Alias = string.Format("{0}id", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = "Id", - Value = Convert.ToInt32(display.Id).ToInvariantString() + "
    " + display.Key + "", - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}creator", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "createBy"), - Description = ui.Text("content", "createByDesc"), //TODO: Localize this - Value = display.Owner.Name, - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}createdate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "createDate"), - Description = ui.Text("content", "createDateDesc"), - Value = display.CreateDate.ToIsoString(), - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}updatedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "updateDate"), - Description = ui.Text("content", "updateDateDesc"), - Value = display.UpdateDate.ToIsoString(), - View = labelEditor - }, - new ContentPropertyDisplay - { - Alias = string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = ui.Text("content", "documentType"), - Value = TranslateItem(display.ContentTypeName, CreateDictionary()), - View = labelEditor - } - }; + Alias = string.Format("{0}id", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = "Id", + Value = Convert.ToInt32(display.Id).ToInvariantString() + "
    " + display.Key + "", + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}creator", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedTextService.Localize("content/createBy"), + Description = localizedTextService.Localize("content/createByDesc"), + Value = display.Owner.Name, + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}createdate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedTextService.Localize("content/createDate"), + Description = localizedTextService.Localize("content/createDateDesc"), + Value = display.CreateDate.ToIsoString(), + View = labelEditor + }, + new ContentPropertyDisplay + { + Alias = string.Format("{0}updatedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedTextService.Localize("content/updateDate"), + Description = localizedTextService.Localize("content/updateDateDesc"), + Value = display.UpdateDate.ToIsoString(), + View = labelEditor + } + }; - //add the custom ones - contentProps.AddRange(customProperties); + if (customProperties != null) + { + //add the custom ones + contentProps.AddRange(customProperties); + } //now add the user props contentProps.AddRange(currProps); + //callback + if (onGenericPropertiesMapped != null) + { + onGenericPropertiesMapped(contentProps); + } + //re-assign genericProps.Properties = contentProps; - } /// @@ -119,7 +123,8 @@ namespace Umbraco.Web.Models.Mapping /// /// This must be either 'content' or 'media' /// - internal static void AddListView(TabbedContentItem display, string entityType, IDataTypeService dataTypeService) + /// + internal static void AddListView(TabbedContentItem display, string entityType, IDataTypeService dataTypeService, ILocalizedTextService localizedTextService) where TPersisted : IContentBase { int dtdId; @@ -159,7 +164,7 @@ namespace Umbraco.Web.Models.Mapping var listViewTab = new Tab(); listViewTab.Alias = Constants.Conventions.PropertyGroups.ListViewGroupName; - listViewTab.Label = ui.Text("content", "childItems"); + listViewTab.Label = localizedTextService.Localize("content/childItems"); listViewTab.Id = 25; listViewTab.IsActive = true; @@ -179,72 +184,102 @@ namespace Umbraco.Web.Models.Mapping }); listViewTab.Properties = listViewProperties; - //Is there a better way? - var tabs = new List>(); - tabs.Add(listViewTab); - tabs.AddRange(display.Tabs); - display.Tabs = tabs; + SetChildItemsTabPosition(display, listViewConfig, listViewTab); + } + private static void SetChildItemsTabPosition(TabbedContentItem display, + IDictionary listViewConfig, + Tab listViewTab) + where TPersisted : IContentBase + { + // Find position of tab from config + var tabIndexForChildItems = 0; + if (listViewConfig["displayAtTabNumber"] != null && int.TryParse((string)listViewConfig["displayAtTabNumber"], out tabIndexForChildItems)) + { + // Tab position is recorded 1-based but we insert into collection 0-based + tabIndexForChildItems--; + + // Ensure within bounds + if (tabIndexForChildItems < 0) + { + tabIndexForChildItems = 0; + } + + if (tabIndexForChildItems > display.Tabs.Count()) + { + tabIndexForChildItems = display.Tabs.Count(); + } + } + + // Recreate tab list with child items tab at configured position + var tabs = new List>(); + tabs.AddRange(display.Tabs); + tabs.Insert(tabIndexForChildItems, listViewTab); + display.Tabs = tabs; } protected override IEnumerable> ResolveCore(IContentBase content) { - var aggregateTabs = new List>(); + var tabs = new List>(); - //now we need to aggregate the tabs and properties since we might have duplicate tabs (based on aliases) because - // of how content composition works. - foreach (var propertyGroups in content.PropertyGroups.OrderBy(x => x.SortOrder).GroupBy(x => x.Name)) + // add the tabs, for properties that belong to a tab + // need to aggregate the tabs, as content.PropertyGroups contains all the composition tabs, + // and there might be duplicates (content does not work like contentType and there is no + // content.CompositionPropertyGroups). + var groupsGroupsByName = content.PropertyGroups.OrderBy(x => x.SortOrder).GroupBy(x => x.Name); + foreach (var groupsByName in groupsGroupsByName) { - var aggregateProperties = new List(); + var properties = new List(); - //add the properties from each composite property group - foreach (var current in propertyGroups) + // merge properties for groups with the same name + foreach (var group in groupsByName) { - var propsForGroup = content.GetPropertiesForGroup(current) - .Where(x => IgnoreProperties.Contains(x.Alias) == false); //don't include ignored props + var groupProperties = content.GetPropertiesForGroup(group) + .Where(x => IgnoreProperties.Contains(x.Alias) == false); // skip ignored - aggregateProperties.AddRange( - Mapper.Map, IEnumerable>( - propsForGroup)); + properties.AddRange(Mapper.Map, IEnumerable>(groupProperties)); } - - if (aggregateProperties.Count == 0) + + if (properties.Count == 0) continue; - TranslateProperties(aggregateProperties); + TranslateProperties(properties); - //then we'll just use the root group's data to make the composite tab - var rootGroup = propertyGroups.First(x => x.ParentId == null); - aggregateTabs.Add(new Tab - { - Id = rootGroup.Id, - Alias = rootGroup.Name, - Label = TranslateItem(rootGroup.Name), - Properties = aggregateProperties, - IsActive = false - }); + // add the tab + // we need to pick an identifier... there is no "right" way... + var g = groupsByName.FirstOrDefault(x => x.Id == content.ContentTypeId) // try local + ?? groupsByName.First(); // else pick one randomly + var groupId = g.Id; + var groupName = groupsByName.Key; + tabs.Add(new Tab + { + Id = groupId, + Alias = groupName, + Label = _localizedTextService.UmbracoDictionaryTranslate(groupName), + Properties = properties, + IsActive = false + }); } - //now add the generic properties tab for any properties that don't belong to a tab - var orphanProperties = content.GetNonGroupedProperties() - .Where(x => IgnoreProperties.Contains(x.Alias) == false); //don't include ignored props - - //now add the generic properties tab - var genericproperties = Mapper.Map, IEnumerable>(orphanProperties).ToList(); + // add the generic properties tab, for properties that don't belong to a tab + // get the properties, map and translate them, then add the tab + var noGroupProperties = content.GetNonGroupedProperties() + .Where(x => IgnoreProperties.Contains(x.Alias) == false); // skip ignored + var genericproperties = Mapper.Map, IEnumerable>(noGroupProperties).ToList(); TranslateProperties(genericproperties); - aggregateTabs.Add(new Tab - { - Id = 0, - Label = ui.Text("general", "properties"), - Alias = "Generic properties", - Properties = genericproperties - }); + tabs.Add(new Tab + { + Id = 0, + Label = _localizedTextService.Localize("general/properties"), + Alias = "Generic properties", + Properties = genericproperties + }); - //set the first tab to active - aggregateTabs.First().IsActive = true; + // activate the first tab + tabs.First().IsActive = true; - return aggregateTabs; + return tabs; } private void TranslateProperties(IEnumerable properties) @@ -252,45 +287,9 @@ namespace Umbraco.Web.Models.Mapping // Not sure whether it's a good idea to add this to the ContentPropertyDisplay mapper foreach (var prop in properties) { - prop.Label = TranslateItem(prop.Label); - prop.Description = TranslateItem(prop.Description); + prop.Label = _localizedTextService.UmbracoDictionaryTranslate(prop.Label); + prop.Description = _localizedTextService.UmbracoDictionaryTranslate(prop.Description); } } - - // TODO: This should really be centralized and used anywhere globalization applies. - internal string TranslateItem(string text) - { - var cultureDictionary = CultureDictionary; - return TranslateItem(text, cultureDictionary); - } - - private static string TranslateItem(string text, ICultureDictionary cultureDictionary) - { - if (text == null) - { - return null; - } - - if (text.StartsWith("#") == false) - return text; - - text = text.Substring(1); - return cultureDictionary[text].IfNullOrWhiteSpace(text); - } - - private ICultureDictionary CultureDictionary - { - get - { - return - _cultureDictionary ?? - (_cultureDictionary = CreateDictionary()); - } - } - - private static ICultureDictionary CreateDictionary() - { - return CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary(); - } } } diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index afae0eb122..72093b19a8 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); config.CreateMap() - .ConstructUsing((IUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' + .ConstructUsing((IUser user) => new UserData()) .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) @@ -52,7 +52,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); - + } private static int GetIntId(object id) diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index aa801fbe2f..98d7085a78 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; @@ -28,56 +31,67 @@ namespace Umbraco.Web.Models /// public virtual string Url { - get - { - // should be thread-safe although it won't prevent url from being resolved more than once - if (_url != null) - return _url; + get + { + // should be thread-safe although it won't prevent url from being resolved more than once + if (_url != null) + return _url; - switch (ItemType) - { - case PublishedItemType.Content: - if (UmbracoContext.Current == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current is null."); - if (UmbracoContext.Current.UrlProvider == null) - throw new InvalidOperationException("Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); - _url= UmbracoContext.Current.UrlProvider.GetUrl(Id); - break; - case PublishedItemType.Media: - var prop = GetProperty(Constants.Conventions.Media.File); - if (prop == null) - throw new NotSupportedException("Cannot resolve a Url for a media item when there is no 'umbracoFile' property defined."); + switch (ItemType) + { + case PublishedItemType.Content: + if (UmbracoContext.Current == null) + throw new InvalidOperationException( + "Cannot resolve a Url for a content item when UmbracoContext.Current is null."); + if (UmbracoContext.Current.UrlProvider == null) + throw new InvalidOperationException( + "Cannot resolve a Url for a content item when UmbracoContext.Current.UrlProvider is null."); + _url = UmbracoContext.Current.UrlProvider.GetUrl(Id); + break; + case PublishedItemType.Media: + var prop = GetProperty(Constants.Conventions.Media.File); + if (prop == null || prop.Value == null) + { + _url = string.Empty; + return _url; + } - if (prop.Value == null) - { - _url = string.Empty; - return _url; - } + var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - var propType = ContentType.GetPropertyType(Constants.Conventions.Media.File); - - //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always - // want to return the normal URL and the cropper stores data as json - switch (propType.PropertyEditorAlias) - { - case Constants.PropertyEditors.UploadFieldAlias: - _url = prop.Value.ToString(); - break; - case Constants.PropertyEditors.ImageCropperAlias: - //get the url from the json format - var val = prop.Value.ToString(); - var crops = val.SerializeToCropDataSet(); - _url = crops != null ? crops.Src : string.Empty; - break; - } - - break; - default: - throw new NotSupportedException(); - } + //This is a hack - since we now have 2 properties that support a URL: upload and cropper, we need to detect this since we always + // want to return the normal URL and the cropper stores data as json + switch (propType.PropertyEditorAlias) + { + case Constants.PropertyEditors.UploadFieldAlias: + _url = prop.Value.ToString(); + break; + case Constants.PropertyEditors.ImageCropperAlias: + //get the url from the json format - return _url; - } + var stronglyTyped = prop.Value as ImageCropDataSet; + if (stronglyTyped != null) + { + _url = stronglyTyped.Src; + break; + } + + var json = prop.Value as JObject; + if (json != null) + { + _url = json.ToObject(new JsonSerializer { Culture = CultureInfo.InvariantCulture, FloatParseHandling = FloatParseHandling.Decimal }).Src; + break; + } + + _url = prop.Value.ToString(); + break; + } + break; + default: + throw new NotSupportedException(); + } + + return _url; + } } public abstract PublishedItemType ItemType { get; } diff --git a/src/Umbraco.Web/Models/RequestPasswordResetModel.cs b/src/Umbraco.Web/Models/RequestPasswordResetModel.cs new file mode 100644 index 0000000000..0ea173bfd6 --- /dev/null +++ b/src/Umbraco.Web/Models/RequestPasswordResetModel.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + + [DataContract(Name = "requestPasswordReset", Namespace = "")] + public class RequestPasswordResetModel + { + [Required] + [DataMember(Name = "email", IsRequired = true)] + public string Email { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/SetPasswordModel.cs b/src/Umbraco.Web/Models/SetPasswordModel.cs new file mode 100644 index 0000000000..02d0e4f901 --- /dev/null +++ b/src/Umbraco.Web/Models/SetPasswordModel.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + [DataContract(Name = "setPassword", Namespace = "")] + public class SetPasswordModel + { + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } + + [Required] + [DataMember(Name = "password", IsRequired = true)] + public string Password { get; set; } + + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string ResetCode { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/Trees/TreeNode.cs b/src/Umbraco.Web/Models/Trees/TreeNode.cs index c413edea37..5c8d36dd6b 100644 --- a/src/Umbraco.Web/Models/Trees/TreeNode.cs +++ b/src/Umbraco.Web/Models/Trees/TreeNode.cs @@ -2,6 +2,7 @@ using Umbraco.Core.IO; using System.Collections.Generic; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Trees @@ -109,7 +110,7 @@ namespace Umbraco.Web.Models.Trees return IOHelper.ResolveUrl("~" + Icon.TrimStart('~')); //legacy icon path - return IOHelper.ResolveUrl("~/umbraco/images/umbraco/" + Icon); + return string.Format("{0}images/umbraco/{1}", GlobalSettings.Path.EnsureEndsWith("/"), Icon); } } diff --git a/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs b/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs new file mode 100644 index 0000000000..cba92eeff7 --- /dev/null +++ b/src/Umbraco.Web/Models/ValidatePasswordResetCodeModel.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + [DataContract(Name = "validatePasswordReset", Namespace = "")] + public class ValidatePasswordResetCodeModel + { + [Required] + [DataMember(Name = "userId", IsRequired = true)] + public int UserId { get; set; } + + [Required] + [DataMember(Name = "resetCode", IsRequired = true)] + public string ResetCode { get; set; } + } +} diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs index 2836c458fb..982286c24b 100644 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs @@ -110,7 +110,7 @@ namespace Umbraco.Web.Mvc //match this area controllerPluginRoute.DataTokens.Add("area", area.AreaName); - controllerPluginRoute.DataTokens.Add("umbraco", umbracoTokenValue); //ensure the umbraco token is set + controllerPluginRoute.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, umbracoTokenValue); //ensure the umbraco token is set return controllerPluginRoute; } diff --git a/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs b/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs new file mode 100644 index 0000000000..c998ed931e --- /dev/null +++ b/src/Umbraco.Web/Mvc/ControllerContextExtensions.cs @@ -0,0 +1,50 @@ +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + public static class ControllerContextExtensions + { + /// + /// Tries to get the Umbraco context from the whole ControllerContext hierarchy based on data tokens and if that fails + /// it will attempt to fallback to retrieving it from the HttpContext. + /// + /// + /// + public static UmbracoContext GetUmbracoContext(this ControllerContext controllerContext) + { + var umbCtx = controllerContext.RouteData.GetUmbracoContext(); + if (umbCtx != null) return umbCtx; + + if (controllerContext.ParentActionViewContext != null) + { + //recurse + return controllerContext.ParentActionViewContext.GetUmbracoContext(); + } + + //fallback to getting from HttpContext + return controllerContext.HttpContext.GetUmbracoContext(); + } + + /// + /// Find a data token in the whole ControllerContext hierarchy of execution + /// + /// + /// + /// + internal static object GetDataTokenInViewContextHierarchy(this ControllerContext controllerContext, string dataTokenName) + { + if (controllerContext.RouteData.DataTokens.ContainsKey(dataTokenName)) + { + return controllerContext.RouteData.DataTokens[dataTokenName]; + } + + if (controllerContext.ParentActionViewContext != null) + { + //recurse! + return controllerContext.ParentActionViewContext.GetDataTokenInViewContextHierarchy(dataTokenName); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/ControllerExtensions.cs b/src/Umbraco.Web/Mvc/ControllerExtensions.cs index c15520b57c..734e60e8f5 100644 --- a/src/Umbraco.Web/Mvc/ControllerExtensions.cs +++ b/src/Umbraco.Web/Mvc/ControllerExtensions.cs @@ -5,24 +5,8 @@ using System.Web.Mvc; namespace Umbraco.Web.Mvc { - internal static class ControllerExtensions + internal static class ControllerExtensions { - internal static object GetDataTokenInViewContextHierarchy(this ControllerContext controllerContext, string dataTokenName) - { - if (controllerContext.RouteData.DataTokens.ContainsKey(dataTokenName)) - { - return controllerContext.RouteData.DataTokens[dataTokenName]; - } - - if (controllerContext.ParentActionViewContext != null) - { - //recurse! - return controllerContext.ParentActionViewContext.GetDataTokenInViewContextHierarchy(dataTokenName); - } - - return null; - } - /// /// Return the controller name from the controller type /// diff --git a/src/Umbraco.Web/Mvc/DefaultRenderMvcControllerResolver.cs b/src/Umbraco.Web/Mvc/DefaultRenderMvcControllerResolver.cs index 715fc674a7..641491c5ba 100644 --- a/src/Umbraco.Web/Mvc/DefaultRenderMvcControllerResolver.cs +++ b/src/Umbraco.Web/Mvc/DefaultRenderMvcControllerResolver.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text; using System.Web.Mvc; @@ -36,6 +37,8 @@ namespace Umbraco.Web.Mvc /// /// Returns an instance of the default controller instance. /// + [Obsolete("This method will be removed in future versions and should not be used to resolve a controller instance, the IControllerFactory is used for that purpose")] + [EditorBrowsable(EditorBrowsableState.Never)] public IRenderMvcController GetControllerInstance() { //try the dependency resolver, then the activator @@ -64,9 +67,9 @@ namespace Umbraco.Web.Mvc /// private void ValidateType(Type type) { - if (TypeHelper.IsTypeAssignableFrom(type) == false) + if (TypeHelper.IsTypeAssignableFrom(type) == false) { - throw new InvalidOperationException("The Type specified (" + type + ") is not of type " + typeof(IRenderMvcController)); + throw new InvalidOperationException("The Type specified (" + type + ") is not of type " + typeof (IRenderController)); } } diff --git a/src/Umbraco.Web/Mvc/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web/Mvc/DisableBrowserCacheAttribute.cs new file mode 100644 index 0000000000..c1c2186171 --- /dev/null +++ b/src/Umbraco.Web/Mvc/DisableBrowserCacheAttribute.cs @@ -0,0 +1,31 @@ +using System; +using System.Web; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// Ensures that the request is not cached by the browser + /// + public class DisableBrowserCacheAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext filterContext) + { + base.OnActionExecuted(filterContext); + + // could happens if exception (but afaik this wouldn't happen in MVC) + if (filterContext.HttpContext == null || filterContext.HttpContext.Response == null || + filterContext.HttpContext.Response.Cache == null) + { + return; + } + + filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); + filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.Zero); + filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + filterContext.HttpContext.Response.Cache.SetNoStore(); + filterContext.HttpContext.Response.AddHeader("Pragma", "no-cache"); + filterContext.HttpContext.Response.Cache.SetExpires(new DateTime(1990, 1, 1, 0, 0, 0)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs new file mode 100644 index 0000000000..625b67b2b2 --- /dev/null +++ b/src/Umbraco.Web/Mvc/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -0,0 +1,73 @@ +using System.IO; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// This is a special filter which is required for the RTE to be able to render Partial View Macros that + /// contain forms when the RTE value is resolved outside of an MVC view being rendered + /// + /// + /// The entire way that we support partial view macros that contain forms isn't really great, these forms + /// need to be executed as ChildActions so that the ModelState,ViewData,TempData get merged into that action + /// so the form can show errors, viewdata, etc... + /// Under normal circumstances, macros will be rendered after a ViewContext is created but in some cases + /// developers will resolve the RTE value in the controller, in this case the Form won't be rendered correctly + /// with merged ModelState from the controller because the special DataToken hasn't been set yet (which is + /// normally done in the UmbracoViewPageOfModel when a real ViewContext is available. + /// So we need to detect if the currently rendering controller is IRenderController and if so we'll ensure that + /// this DataToken exists before the action executes in case the developer resolves an RTE value that contains + /// a partial view macro form. + /// + internal class EnsurePartialViewMacroViewContextFilterAttribute : ActionFilterAttribute + { + /// + /// Ensures the custom ViewContext datatoken is set before the RenderController action is invoked, + /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still + /// render any PartialViewMacro with a form and maintain ModelState + /// + /// + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + //ignore anything that is not IRenderController + if ((filterContext.Controller is IRenderController) == false && filterContext.IsChildAction == false) + return; + + SetViewContext(filterContext); + } + + /// + /// Ensures that the custom ViewContext datatoken is set after the RenderController action is invoked, + /// this ensures that any custom ModelState that may have been added in the RenderController itself is + /// passed onwards in case it is required when rendering a PartialViewMacro with a form + /// + /// The filter context. + public override void OnResultExecuting(ResultExecutingContext filterContext) + { + //ignore anything that is not IRenderController + if ((filterContext.Controller is IRenderController) == false && filterContext.IsChildAction == false) + return; + + SetViewContext(filterContext); + } + + private void SetViewContext(ControllerContext controllerContext) + { + var viewCtx = new ViewContext( + controllerContext, + new DummyView(), + controllerContext.Controller.ViewData, controllerContext.Controller.TempData, + new StringWriter()); + + //set the special data token + controllerContext.RequestContext.RouteData.DataTokens[Constants.DataTokenCurrentViewContext] = viewCtx; + } + + private class DummyView : IView + { + public void Render(ViewContext viewContext, TextWriter writer) + { + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/IRenderController.cs b/src/Umbraco.Web/Mvc/IRenderController.cs new file mode 100644 index 0000000000..e4303e06e5 --- /dev/null +++ b/src/Umbraco.Web/Mvc/IRenderController.cs @@ -0,0 +1,12 @@ +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking + /// + public interface IRenderController : IController + { + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/IRenderMvcController.cs b/src/Umbraco.Web/Mvc/IRenderMvcController.cs index 755ccd5854..53865cd4ee 100644 --- a/src/Umbraco.Web/Mvc/IRenderMvcController.cs +++ b/src/Umbraco.Web/Mvc/IRenderMvcController.cs @@ -1,5 +1,7 @@ +using System; using System.Web.Http.Filters; using System.Web.Mvc; +using System.Web.Routing; using System.Windows.Forms; using Umbraco.Web.Models; @@ -8,7 +10,7 @@ namespace Umbraco.Web.Mvc /// /// The interface that must be implemented for a controller to be designated to execute for route hijacking /// - public interface IRenderMvcController : IController + public interface IRenderMvcController : IRenderController { /// /// The default action to render the front-end view diff --git a/src/Umbraco.Web/Mvc/ModelBindingException.cs b/src/Umbraco.Web/Mvc/ModelBindingException.cs new file mode 100644 index 0000000000..d675ae4a65 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ModelBindingException.cs @@ -0,0 +1,14 @@ +using System; + +namespace Umbraco.Web.Mvc +{ + public class ModelBindingException : Exception + { + public ModelBindingException() + { } + + public ModelBindingException(string message) + : base(message) + { } + } +} diff --git a/src/Umbraco.Web/Mvc/RenderActionInvoker.cs b/src/Umbraco.Web/Mvc/RenderActionInvoker.cs index f6b3c673bf..55be6591ad 100644 --- a/src/Umbraco.Web/Mvc/RenderActionInvoker.cs +++ b/src/Umbraco.Web/Mvc/RenderActionInvoker.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using System.Web.Mvc; using System.Web.Mvc.Async; @@ -10,9 +12,8 @@ namespace Umbraco.Web.Mvc /// Ensures that if an action for the Template name is not explicitly defined by a user, that the 'Index' action will execute /// public class RenderActionInvoker : AsyncControllerActionInvoker - { + { - private static readonly ConcurrentDictionary IndexDescriptors = new ConcurrentDictionary(); /// /// Ensures that if an action for the Template name is not explicitly defined by a user, that the 'Index' action will execute @@ -28,23 +29,14 @@ namespace Umbraco.Web.Mvc //now we need to check if it exists, if not we need to return the Index by default if (ad == null) { - //check if the controller is an instance of IRenderMvcController - if (controllerContext.Controller is IRenderMvcController) + //check if the controller is an instance of IRenderController and find the index + if (controllerContext.Controller is IRenderController) { - return IndexDescriptors.GetOrAdd(controllerContext.Controller.GetType(), - type => new ReflectedActionDescriptor( - controllerContext.Controller.GetType().GetMethods() - .First(x => x.Name == "Index" && - x.GetCustomAttributes(typeof (NonActionAttribute), false).Any() == false), - "Index", - controllerDescriptor)); - - - - } + return controllerDescriptor.FindAction(controllerContext, "Index"); + } } return ad; } - + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs b/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs new file mode 100644 index 0000000000..1d1c56db11 --- /dev/null +++ b/src/Umbraco.Web/Mvc/RenderIndexActionSelectorAttribute.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Reflection; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// A custom ActionMethodSelector which will ensure that the RenderMvcController.Index(RenderModel model) action will be executed + /// if the + /// + internal class RenderIndexActionSelectorAttribute : ActionMethodSelectorAttribute + { + private static readonly ConcurrentDictionary ControllerDescCache = new ConcurrentDictionary(); + + /// + /// Determines whether the action method selection is valid for the specified controller context. + /// + /// + /// true if the action method selection is valid for the specified controller context; otherwise, false. + /// + /// The controller context.Information about the action method. + public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) + { + var currType = methodInfo.ReflectedType; + var baseType = methodInfo.DeclaringType; + + //It's the same type, so this must be the Index action to use + if (currType == baseType) return true; + + if (currType == null) return false; + + var controllerDesc = ControllerDescCache.GetOrAdd(currType, type => new ReflectedControllerDescriptor(currType)); + var actions = controllerDesc.GetCanonicalActions(); + + //If there are more than one Index action for this controller, then + // this base class one should not be matched + return actions.Count(x => x.ActionName == "Index") <= 1; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderModelBinder.cs b/src/Umbraco.Web/Mvc/RenderModelBinder.cs index f8b1df7520..735f015afc 100644 --- a/src/Umbraco.Web/Mvc/RenderModelBinder.cs +++ b/src/Umbraco.Web/Mvc/RenderModelBinder.cs @@ -1,11 +1,20 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Web; using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Web.Models; namespace Umbraco.Web.Mvc { - public class RenderModelBinder : IModelBinder - { - + /// + /// Allows for Model Binding any IPublishedContent or IRenderModel + /// + public class RenderModelBinder : DefaultModelBinder, IModelBinder, IModelBinderProvider + { /// /// Binds the model to a value by using the specified controller context and binding context. /// @@ -13,19 +22,167 @@ namespace Umbraco.Web.Mvc /// The bound value. /// /// The controller context.The binding context. - public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) + public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { - var requestMatchesType = typeof(RenderModel) == bindingContext.ModelType; + object model; + if (controllerContext.RouteData.DataTokens.TryGetValue(Core.Constants.Web.UmbracoDataToken, out model) == false) + return null; - if (requestMatchesType) - { - //get the model from the route data - if (!controllerContext.RouteData.DataTokens.ContainsKey("umbraco")) - return null; - var model = controllerContext.RouteData.DataTokens["umbraco"] as RenderModel; - return model; - } - return null; + //This model binder deals with IRenderModel and IPublishedContent by extracting the model from the route's + // datatokens. This data token is set in 2 places: RenderRouteHandler, UmbracoVirtualNodeRouteHandler + // and both always set the model to an instance of `RenderModel`. So if this isn't an instance of IRenderModel then + // we need to let the DefaultModelBinder deal with the logic. + var renderModel = model as IRenderModel; + if (renderModel == null) + { + model = base.BindModel(controllerContext, bindingContext); + if (model == null) return null; + } + + //if for any reason the model is not either IRenderModel or IPublishedContent, then we return since those are the only + // types this binder is dealing with. + if ((model is IRenderModel) == false && (model is IPublishedContent) == false) return null; + + //default culture + var culture = CultureInfo.CurrentCulture; + + var umbracoContext = controllerContext.GetUmbracoContext() + ?? UmbracoContext.Current; + + if (umbracoContext != null && umbracoContext.PublishedContentRequest != null) + { + culture = umbracoContext.PublishedContentRequest.Culture; + } + + return BindModel(model, bindingContext.ModelType, culture); } - } + + // source is the model that we have + // modelType is the type of the model that we need to bind to + // culture is the CultureInfo that we have, used by RenderModel + // + // create a model object of the modelType by mapping: + // { RenderModel, RenderModel, IPublishedContent } + // to + // { RenderModel, RenderModel, IPublishedContent } + // + public static object BindModel(object source, Type modelType, CultureInfo culture) + { + // null model, return + if (source == null) return null; + + // if types already match, return + var sourceType = source.GetType(); + if (sourceType.Inherits(modelType)) // includes == + return source; + + // try to grab the content + var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent + if (sourceContent == null && sourceType.Implements()) + { + // else check if it's an IRenderModel, and get the content + sourceContent = ((IRenderModel)source).Content; + } + if (sourceContent == null) + { + // else check if we can convert it to a content + var attempt1 = source.TryConvertTo(); + if (attempt1.Success) sourceContent = attempt1.Result; + } + + // if we have a content + if (sourceContent != null) + { + // try to grab the culture + // using supplied culture by default + var sourceRenderModel = source as RenderModel; + if (sourceRenderModel != null) + culture = sourceRenderModel.CurrentCulture; + + // if model is IPublishedContent, check content type and return + if (modelType.Implements()) + { + if ((sourceContent.GetType().Inherits(modelType)) == false) + ThrowModelBindingException(true, false, sourceContent.GetType(), modelType); + return sourceContent; + } + + // if model is RenderModel, create and return + if (modelType == typeof(RenderModel)) + { + return new RenderModel(sourceContent, culture); + } + + // if model is RenderModel, check content type, then create and return + if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(RenderModel<>)) + { + var targetContentType = modelType.GetGenericArguments()[0]; + if ((sourceContent.GetType().Inherits(targetContentType)) == false) + ThrowModelBindingException(true, true, sourceContent.GetType(), targetContentType); + return Activator.CreateInstance(modelType, sourceContent, culture); + } + } + + // last chance : try to convert + var attempt2 = source.TryConvertTo(modelType); + if (attempt2.Success) return attempt2.Result; + + // fail + ThrowModelBindingException(false, false, sourceType, modelType); + return null; + } + + private static void ThrowModelBindingException(bool sourceContent, bool modelContent, Type sourceType, Type modelType) + { + var msg = new StringBuilder(); + + msg.Append("Cannot bind source"); + if (sourceContent) msg.Append(" content"); + msg.Append(" type "); + msg.Append(sourceType.FullName); + msg.Append(" to model"); + if (modelContent) msg.Append(" content"); + msg.Append(" type "); + msg.Append(modelType.FullName); + msg.Append("."); + + // compare FullName for the time being because when upgrading ModelsBuilder, + // Umbraco does not know about the new attribute type - later on, can compare + // on type directly (ie after v7.4.2). + var sourceAttr = sourceType.Assembly.CustomAttributes.FirstOrDefault(x => + x.AttributeType.FullName == "Umbraco.ModelsBuilder.PureLiveAssemblyAttribute"); + var modelAttr = modelType.Assembly.CustomAttributes.FirstOrDefault(x => + x.AttributeType.FullName == "Umbraco.ModelsBuilder.PureLiveAssemblyAttribute"); + + // bah.. names are App_Web_all.generated.cs.8f9494c4.jjuvxz55 so they ARE different, fuck! + // we cannot compare purely on type.FullName 'cos we might be trying to map Sub to Main = fails! + if (sourceAttr != null && modelAttr != null + && sourceType.Assembly.GetName().Version.Revision != modelType.Assembly.GetName().Version.Revision) + { + msg.Append(" Types come from two PureLive assemblies with different versions,"); + msg.Append(" this usually indicates that the application is in an unstable state."); + msg.Append(" The application is restarting now, reload the page and it should work."); + var context = HttpContext.Current; + if (context == null) + AppDomain.Unload(AppDomain.CurrentDomain); + else + ApplicationContext.Current.RestartApplicationPool(new HttpContextWrapper(context)); + } + + throw new ModelBindingException(msg.ToString()); + } + + public IModelBinder GetBinder(Type modelType) + { + // can bind to RenderModel (exact type match) + if (modelType == typeof(RenderModel)) return this; + + // can bind to RenderModel (exact generic type match) + if (modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(RenderModel<>)) return this; + + // can bind to TContent where TContent : IPublishedContent (any IPublishedContent implementation) + if (typeof(IPublishedContent).IsAssignableFrom(modelType)) return this; + return null; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderMvcController.cs b/src/Umbraco.Web/Mvc/RenderMvcController.cs index 03f93d89e3..f3886d8ce1 100644 --- a/src/Umbraco.Web/Mvc/RenderMvcController.cs +++ b/src/Umbraco.Web/Mvc/RenderMvcController.cs @@ -1,22 +1,17 @@ using System; -using System.IO; using System.Web.Mvc; -using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Core.Models; using Umbraco.Web.Models; using Umbraco.Web.Routing; -using Umbraco.Web.Security; namespace Umbraco.Web.Mvc { /// - /// A controller to render front-end requests + /// The default controller to render front-end requests /// - [PreRenderViewActionFilter] + [PreRenderViewActionFilter] public class RenderMvcController : UmbracoController, IRenderMvcController { @@ -64,11 +59,11 @@ namespace Umbraco.Web.Mvc { if (_publishedContentRequest != null) return _publishedContentRequest; - if (!RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + if (RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken) == false) { throw new InvalidOperationException("DataTokens must contain an 'umbraco-doc-request' key with a PublishedContentRequest object"); } - _publishedContentRequest = (PublishedContentRequest)RouteData.DataTokens["umbraco-doc-request"]; + _publishedContentRequest = (PublishedContentRequest)RouteData.DataTokens[Core.Constants.Web.PublishedDocumentRequestDataToken]; return _publishedContentRequest; } } @@ -111,10 +106,10 @@ namespace Umbraco.Web.Mvc /// /// /// + [RenderIndexActionSelector] public virtual ActionResult Index(RenderModel model) { return CurrentTemplate(model); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 64dcb2a04b..d7ce36b4a4 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -97,9 +97,9 @@ namespace Umbraco.Web.Mvc internal void SetupRouteDataForRequest(RenderModel renderModel, RequestContext requestContext, PublishedContentRequest docRequest) { //put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine - requestContext.RouteData.DataTokens.Add("umbraco", renderModel); //required for the RenderModelBinder and view engine - requestContext.RouteData.DataTokens.Add("umbraco-doc-request", docRequest); //required for RenderMvcController - requestContext.RouteData.DataTokens.Add("umbraco-context", UmbracoContext); //required for UmbracoTemplatePage + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, renderModel); //required for the RenderModelBinder and view engine + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, docRequest); //required for RenderMvcController + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, UmbracoContext); //required for UmbracoTemplatePage } private void UpdateRouteDataForRequest(RenderModel renderModel, RequestContext requestContext) @@ -107,7 +107,7 @@ namespace Umbraco.Web.Mvc if (renderModel == null) throw new ArgumentNullException("renderModel"); if (requestContext == null) throw new ArgumentNullException("requestContext"); - requestContext.RouteData.DataTokens["umbraco"] = renderModel; + requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken] = renderModel; // the rest should not change -- it's only the published content that has changed } @@ -310,8 +310,8 @@ namespace Umbraco.Web.Mvc //check if that controller exists if (controllerType != null) { - //ensure the controller is of type 'IRenderMvcController' and ControllerBase - if (TypeHelper.IsTypeAssignableFrom(controllerType) + //ensure the controller is of type IRenderMvcController and ControllerBase + if (TypeHelper.IsTypeAssignableFrom(controllerType) && TypeHelper.IsTypeAssignableFrom(controllerType)) { //set the controller and name to the custom one @@ -328,7 +328,7 @@ namespace Umbraco.Web.Mvc "The current Document Type {0} matches a locally declared controller of type {1}. Custom Controllers for Umbraco routing must implement '{2}' and inherit from '{3}'.", () => publishedContentRequest.PublishedContent.DocumentTypeAlias, () => controllerType.FullName, - () => typeof(IRenderMvcController).FullName, + () => typeof(IRenderController).FullName, () => typeof(ControllerBase).FullName); //we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults @@ -337,7 +337,7 @@ namespace Umbraco.Web.Mvc } //store the route definition - requestContext.RouteData.DataTokens["umbraco-route-def"] = def; + requestContext.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; return def; } diff --git a/src/Umbraco.Web/Mvc/RenderViewEngine.cs b/src/Umbraco.Web/Mvc/RenderViewEngine.cs index ed7b941580..fdf61059e0 100644 --- a/src/Umbraco.Web/Mvc/RenderViewEngine.cs +++ b/src/Umbraco.Web/Mvc/RenderViewEngine.cs @@ -93,7 +93,7 @@ namespace Umbraco.Web.Mvc /// private bool ShouldFindView(ControllerContext controllerContext, bool isPartial) { - var umbracoToken = controllerContext.GetDataTokenInViewContextHierarchy("umbraco"); + var umbracoToken = controllerContext.GetDataTokenInViewContextHierarchy(Core.Constants.Web.UmbracoDataToken); //first check if we're rendering a partial view for the back office, or surface controller, etc... //anything that is not IUmbracoRenderModel as this should only pertain to Umbraco views. diff --git a/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs b/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs index 445441aafa..ceb4012a66 100644 --- a/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs +++ b/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Mvc public static object GetRequiredObject(this RouteValueDictionary items, string key) { if (key == null) throw new ArgumentNullException("key"); - if (!items.Keys.Contains(key)) + if (items.Keys.Contains(key) == false) throw new ArgumentNullException("The " + key + " parameter was not found but is required"); return items[key]; } diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index 817ed97902..420573d745 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -185,9 +185,9 @@ namespace Umbraco.Web.Mvc while (currentContext != null) { var currentRouteData = currentContext.RouteData; - if (currentRouteData.DataTokens.ContainsKey("umbraco-route-def")) + if (currentRouteData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) { - return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens["umbraco-route-def"]); + return Attempt.Succeed((RouteDefinition)currentRouteData.DataTokens[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); } if (currentContext.IsChildAction) { diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs b/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs index c82c59db65..8af018e833 100644 --- a/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.Mvc return false; //ensure there is an umbraco token set - var umbracoToken = request.RouteData.DataTokens["umbraco"]; + var umbracoToken = request.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken]; if (umbracoToken == null || string.IsNullOrWhiteSpace(umbracoToken.ToString())) return false; diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs index a70bc47cb9..9229939d0f 100644 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs +++ b/src/Umbraco.Web/Mvc/UmbracoAuthorizedController.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; -using System.Web.Mvc; using Umbraco.Core.Configuration; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -18,6 +17,7 @@ namespace Umbraco.Web.Mvc /// authorization of each method can use this attribute instead of inheriting from this controller. /// [UmbracoAuthorize] + [DisableBrowserCache] public abstract class UmbracoAuthorizedController : UmbracoController { diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs index f23515c1d9..da4cb198ac 100644 --- a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Mvc ValidateRouteData(context.RouteData); - var routeDef = (RouteDefinition)context.RouteData.DataTokens["umbraco-route-def"]; + var routeDef = (RouteDefinition)context.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken]; //Special case, if it is webforms but we're posting to an MVC surface controller, then we // need to return the webforms result instead @@ -91,7 +91,7 @@ namespace Umbraco.Web.Mvc /// private static void ValidateRouteData(RouteData routeData) { - if (routeData.DataTokens.ContainsKey("umbraco-route-def") == false) + if (routeData.DataTokens.ContainsKey(Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken) == false) { throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + " in the context of an Http POST when using a SurfaceController form"); diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs index 0cf248fd48..3d1a41d289 100644 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Text; using System.Web; using System.Web.Mvc; @@ -26,24 +27,13 @@ namespace Umbraco.Web.Mvc get { //we should always try to return the context from the data tokens just in case its a custom context and not - //using the UmbracoContext.Current. - //we will fallback to the singleton if necessary. - if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) - { - return (UmbracoContext)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); - } - //next check if it is a child action and see if the parent has it set in data tokens - if (ViewContext.IsChildAction) - { - if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-context")) - { - return (UmbracoContext)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-context"); - } - } - - //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this - //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. - return UmbracoContext.Current; + //using the UmbracoContext.Current, we will fallback to the singleton if necessary. + var umbCtx = ViewContext.GetUmbracoContext() + //lastly, we will use the singleton, the only reason this should ever happen is is someone is rendering a page that inherits from this + //class and are rendering it outside of the normal Umbraco routing process. Very unlikely. + ?? UmbracoContext.Current; + + return umbCtx; } } @@ -65,16 +55,16 @@ namespace Umbraco.Web.Mvc //we should always try to return the object from the data tokens just in case its a custom object and not //using the UmbracoContext.Current. //we will fallback to the singleton if necessary. - if (ViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + if (ViewContext.RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken)) { - return (PublishedContentRequest)ViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); + return (PublishedContentRequest)ViewContext.RouteData.DataTokens.GetRequiredObject(Core.Constants.Web.PublishedDocumentRequestDataToken); } //next check if it is a child action and see if the parent has it set in data tokens if (ViewContext.IsChildAction) { - if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey("umbraco-doc-request")) + if (ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken)) { - return (PublishedContentRequest)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject("umbraco-doc-request"); + return (PublishedContentRequest)ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject(Core.Constants.Web.PublishedDocumentRequestDataToken); } } @@ -130,6 +120,9 @@ namespace Umbraco.Web.Mvc base.InitializePage(); if (ViewContext.IsChildAction == false) { + //this is used purely for partial view macros that contain forms + // and mostly just when rendered within the RTE - This should already be set with the + // EnsurePartialViewMacroViewContextFilterAttribute if (ViewContext.RouteData.DataTokens.ContainsKey(Constants.DataTokenCurrentViewContext) == false) { ViewContext.RouteData.DataTokens.Add(Constants.DataTokenCurrentViewContext, ViewContext); @@ -141,92 +134,58 @@ namespace Umbraco.Web.Mvc // maps model protected override void SetViewData(ViewDataDictionary viewData) { - // if view data contains no model, nothing to do - var source = viewData.Model; - if (source == null) - { - base.SetViewData(viewData); - return; - } + // capture the model before we tinker with the viewData + var viewDataModel = viewData.Model; - // get the type of the view data model (what we have) - // get the type of this view model (what we want) - var sourceType = source.GetType(); - var targetType = typeof (TModel); + // map the view data (may change its type, may set model to null) + viewData = MapViewDataDictionary(viewData, typeof (TModel)); - // it types already match, nothing to do - if (sourceType.Inherits()) // includes == - { - base.SetViewData(viewData); - return; - } - - // try to grab the content - // if no content is found, return, nothing we can do - var sourceContent = source as IPublishedContent; // check if what we have is an IPublishedContent - if (sourceContent == null && sourceType.Implements()) - { - // else check if it's an IRenderModel => get the content - sourceContent = ((IRenderModel)source).Content; - } - if (sourceContent == null) - { - // else check if we can convert it to a content - var attempt = source.TryConvertTo(); - if (attempt.Success) sourceContent = attempt.Result; - } - - var ok = sourceContent != null; - if (sourceContent != null) - { - // try to grab the culture - // using context's culture by default - var culture = UmbracoContext.PublishedContentRequest.Culture; - var sourceRenderModel = source as RenderModel; - if (sourceRenderModel != null) - culture = sourceRenderModel.CurrentCulture; - - // reassign the model depending on its type - if (targetType.Implements()) - { - // it TModel implements IPublishedContent then use the content - // provided that the content is of the proper type - if ((sourceContent is TModel) == false) - throw new InvalidCastException(string.Format("Cannot cast source content type {0} to view model type {1}.", - sourceContent.GetType(), targetType)); - viewData.Model = sourceContent; - } - else if (targetType == typeof(RenderModel)) - { - // if TModel is a basic RenderModel just create it - viewData.Model = new RenderModel(sourceContent, culture); - } - else if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(RenderModel<>)) - { - // if TModel is a strongly-typed RenderModel<> then create it - // provided that the content is of the proper type - var targetContentType = targetType.GetGenericArguments()[0]; - if ((sourceContent.GetType().Inherits(targetContentType)) == false) - throw new InvalidCastException(string.Format("Cannot cast source content type {0} to view model content type {1}.", - sourceContent.GetType(), targetContentType)); - viewData.Model = Activator.CreateInstance(targetType, sourceContent, culture); - } - else - { - ok = false; - } - } - - if (ok == false) - { - // last chance : try to convert - var attempt = source.TryConvertTo(); - if (attempt.Success) viewData.Model = attempt.Result; - } + var culture = CultureInfo.CurrentCulture; + // bind the model (use context culture as default, if available) + if (UmbracoContext.PublishedContentRequest != null && UmbracoContext.PublishedContentRequest.Culture != null) + culture = UmbracoContext.PublishedContentRequest.Culture; + viewData.Model = RenderModelBinder.BindModel(viewDataModel, typeof (TModel), culture); + // set the view data base.SetViewData(viewData); } + // viewData is the ViewDataDictionary (maybe ) that we have + // modelType is the type of the model that we need to bind to + // + // figure out whether viewData can accept modelType else replace it + // + private static ViewDataDictionary MapViewDataDictionary(ViewDataDictionary viewData, Type modelType) + { + var viewDataType = viewData.GetType(); + + // if viewData is not generic then it is a simple ViewDataDictionary instance and its + // Model property is of type 'object' and will accept anything, so it is safe to use + // viewData + if (viewDataType.IsGenericType == false) + return viewData; + + // ensure it is the proper generic type + var def = viewDataType.GetGenericTypeDefinition(); + if (def != typeof(ViewDataDictionary<>)) + throw new Exception("Could not map viewData of type \"" + viewDataType.FullName + "\"."); + + // get the viewData model type and compare with the actual view model type: + // viewData is ViewDataDictionary and we will want to assign an + // object of type modelType to the Model property of type viewDataModelType, we + // need to check whether that is possible + var viewDataModelType = viewDataType.GenericTypeArguments[0]; + + if (viewDataModelType.IsAssignableFrom(modelType)) + return viewData; + + // if not possible then we need to create a new ViewDataDictionary + var nViewDataType = typeof(ViewDataDictionary<>).MakeGenericType(modelType); + var tViewData = new ViewDataDictionary(viewData) { Model = null }; // temp view data to copy values + var nViewData = (ViewDataDictionary)Activator.CreateInstance(nViewDataType, tViewData); + return nViewData; + } + /// /// This will detect the end /body tag and insert the preview badge if in preview mode /// @@ -238,7 +197,7 @@ namespace Umbraco.Web.Mvc { if (UmbracoContext.Current.IsDebug || UmbracoContext.Current.InPreviewMode) { - var text = value.ToString().ToLowerInvariant(); + var text = value.ToString(); var pos = text.IndexOf("", StringComparison.InvariantCultureIgnoreCase); if (pos > -1) @@ -294,4 +253,4 @@ namespace Umbraco.Web.Mvc return WebViewPageExtensions.RenderSection(this, name, defaultContents); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs index feb5b30ce2..5c948d2e0b 100644 --- a/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/UmbracoVirtualNodeRouteHandler.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; +using System.Web.Security; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web.Models; using Umbraco.Web.Routing; @@ -19,7 +21,8 @@ namespace Umbraco.Web.Mvc if (found == null) return new NotFoundHandler(); umbracoContext.PublishedContentRequest = new PublishedContentRequest( - umbracoContext.CleanedUmbracoUrl, umbracoContext.RoutingContext) + umbracoContext.CleanedUmbracoUrl, umbracoContext.RoutingContext, + UmbracoConfig.For.UmbracoSettings().WebRouting, s => Roles.Provider.GetRolesForUser(s)) { PublishedContent = found }; @@ -31,11 +34,11 @@ namespace Umbraco.Web.Mvc var renderModel = new RenderModel(umbracoContext.PublishedContentRequest.PublishedContent, umbracoContext.PublishedContentRequest.Culture); //assigns the required tokens to the request - requestContext.RouteData.DataTokens.Add("umbraco", renderModel); - requestContext.RouteData.DataTokens.Add("umbraco-doc-request", umbracoContext.PublishedContentRequest); - requestContext.RouteData.DataTokens.Add("umbraco-context", umbracoContext); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, renderModel); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.PublishedDocumentRequestDataToken, umbracoContext.PublishedContentRequest); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbracoContext); //this is used just for a flag that this is an umbraco custom route - requestContext.RouteData.DataTokens.Add("umbraco-custom-route", true); + requestContext.RouteData.DataTokens.Add(Core.Constants.Web.CustomRouteDataToken, true); //Here we need to detect if a SurfaceController has posted var formInfo = RenderRouteHandler.GetFormInfo(requestContext); @@ -49,7 +52,7 @@ namespace Umbraco.Web.Mvc }; //set the special data token to the current route definition - requestContext.RouteData.DataTokens["umbraco-route-def"] = def; + requestContext.RouteData.DataTokens[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken] = def; return RenderRouteHandler.HandlePostedValues(requestContext, formInfo); } diff --git a/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs new file mode 100644 index 0000000000..977f37473b --- /dev/null +++ b/src/Umbraco.Web/Mvc/ValidateMvcAngularAntiForgeryTokenAttribute.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Web.Mvc; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Mvc +{ + /// + /// A filter to check for the csrf token based on Angular's standard approach + /// + /// + /// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/ + /// + /// If the authentication type is cookie based, then this filter will execute, otherwise it will be disabled + /// + public sealed class ValidateMvcAngularAntiForgeryTokenAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + var userIdentity = filterContext.HttpContext.User.Identity as ClaimsIdentity; + if (userIdentity != null) + { + //if there is not CookiePath claim, then exist + if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) + { + base.OnActionExecuting(filterContext); + return; + } + } + + string failedReason; + var headers = new List>>(); + foreach (var key in filterContext.HttpContext.Request.Headers.AllKeys) + { + if (headers.Any(x => x.Key == key)) + { + var found = headers.First(x => x.Key == key); + found.Value.Add(filterContext.HttpContext.Request.Headers[key]); + } + else + { + headers.Add(new KeyValuePair>(key, new List { filterContext.HttpContext.Request.Headers[key] })); + } + } + var cookie = filterContext.HttpContext.Request.Cookies[AngularAntiForgeryHelper.CsrfValidationCookieName]; + if (AngularAntiForgeryHelper.ValidateHeaders( + headers.Select(x => new KeyValuePair>(x.Key, x.Value)).ToArray(), + cookie == null ? "" : cookie.Value, + out failedReason) == false) + { + var result = new HttpStatusCodeResult(HttpStatusCode.ExpectationFailed); + filterContext.Result = result; + return; + } + + base.OnActionExecuting(filterContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/OwinMiddlewareConfiguredEventArgs.cs b/src/Umbraco.Web/OwinMiddlewareConfiguredEventArgs.cs new file mode 100644 index 0000000000..03c2dc631d --- /dev/null +++ b/src/Umbraco.Web/OwinMiddlewareConfiguredEventArgs.cs @@ -0,0 +1,15 @@ +using System; +using Owin; + +namespace Umbraco.Web +{ + public class OwinMiddlewareConfiguredEventArgs : EventArgs + { + public OwinMiddlewareConfiguredEventArgs(IAppBuilder appBuilder) + { + AppBuilder = appBuilder; + } + + public IAppBuilder AppBuilder { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index bb5031c32d..89499b833e 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -34,5 +34,7 @@ using System.Security; [assembly: InternalsVisibleTo("Concorde.Sync")] [assembly: InternalsVisibleTo("Umbraco.Courier.Core")] [assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] - +[assembly: InternalsVisibleTo("Umbraco.VisualStudio")] +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] +[assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs index d4c5cdbaf1..4407b2fcdf 100644 --- a/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/CheckBoxListPropertyEditor.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PropertyEditors /// as INT and we have logic in here to ensure it is formatted correctly including ensuring that the string value is published /// in cache and not the int ID. ///
    - [PropertyEditor(Constants.PropertyEditors.CheckBoxListAlias, "Checkbox list", "checkboxlist")] + [PropertyEditor(Constants.PropertyEditors.CheckBoxListAlias, "Checkbox list", "checkboxlist", Icon="icon-bulleted-list", Group="lists")] public class CheckBoxListPropertyEditor : PropertyEditor { diff --git a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs index 40b719020f..0b94ddd993 100644 --- a/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ColorPickerPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.ColorPickerAlias, "Color Picker", "colorpicker")] + [PropertyEditor(Constants.PropertyEditors.ColorPickerAlias, "Color Picker", "colorpicker", Icon="icon-colorpicker", Group="Pickers")] public class ColorPickerPropertyEditor : PropertyEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 1789c6dae6..4d58060164 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -4,17 +4,19 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.ContentPickerAlias, "Content Picker", "INT", "contentpicker", IsParameterEditor = true)] + [PropertyEditor(Constants.PropertyEditors.ContentPickerAlias, "Content Picker", PropertyEditorValueTypes.Integer, "contentpicker", IsParameterEditor = true, Group = "Pickers")] public class ContentPickerPropertyEditor : PropertyEditor { public ContentPickerPropertyEditor() { _internalPreValues = new Dictionary - { - {"showEditButton", "0"}, - {"startNodeId", "-1"} - }; + { + {"startNodeId", "-1"}, + {"showOpenButton", "0"}, + {"showEditButton", "0"}, + {"showPathOnHover", "0"} + }; } private IDictionary _internalPreValues; @@ -31,12 +33,17 @@ namespace Umbraco.Web.PropertyEditors internal class ContentPickerPreValueEditor : PreValueEditor { + [PreValueField("showOpenButton", "Show open button", "boolean")] + public string ShowOpenButton { get; set; } + [PreValueField("showEditButton", "Show edit button (this feature is in preview!)", "boolean")] public string ShowEditButton { get; set; } [PreValueField("startNodeId", "Start node", "treepicker")] public int StartNodeId { get; set; } + [PreValueField("showPathOnHover", "Show path when hovering items", "boolean")] + public bool ShowPathOnHover { get; set; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/DatePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DatePropertyEditor.cs index f8436f0ebd..a98c96e759 100644 --- a/src/Umbraco.Web/PropertyEditors/DatePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DatePropertyEditor.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.DateAlias, "Date", "DATE", "datepicker")] + [PropertyEditor(Constants.PropertyEditors.DateAlias, "Date", PropertyEditorValueTypes.Date, "datepicker", Icon="icon-calendar")] public class DatePropertyEditor : PropertyEditor { public DatePropertyEditor() diff --git a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs index 2b7e01ef5d..6291afd195 100644 --- a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs @@ -5,7 +5,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.DateTimeAlias, "Date/Time", "datepicker", ValueType = "DATETIME")] + [PropertyEditor(Constants.PropertyEditors.DateTimeAlias, "Date/Time", "datepicker", ValueType = PropertyEditorValueTypes.DateTime, Icon="icon-time")] public class DateTimePropertyEditor : PropertyEditor { public DateTimePropertyEditor() diff --git a/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs new file mode 100644 index 0000000000..63c1964668 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/DecimalPropertyEditor.cs @@ -0,0 +1,57 @@ +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.DecimalAlias, "Decimal", PropertyEditorValueTypes.Decimal, "decimal", IsParameterEditor = true)] + public class DecimalPropertyEditor : PropertyEditor + { + /// + /// Overridden to ensure that the value is validated + /// + /// + protected override PropertyValueEditor CreateValueEditor() + { + var editor = base.CreateValueEditor(); + editor.Validators.Add(new DecimalValidator()); + return editor; + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new DecimalPreValueEditor(); + } + + /// + /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. + /// + internal class DecimalPreValueEditor : PreValueEditor + { + public DecimalPreValueEditor() + { + //create the fields + Fields.Add(new PreValueField(new DecimalValidator()) + { + Description = "Enter the minimum amount of number to be entered", + Key = "min", + View = "decimal", + Name = "Minimum" + }); + Fields.Add(new PreValueField(new DecimalValidator()) + { + Description = "Enter the intervals amount between each step of number to be entered", + Key = "step", + View = "decimal", + Name = "Step Size" + }); + Fields.Add(new PreValueField(new DecimalValidator()) + { + Description = "Enter the maximum amount of number to be entered", + Key = "max", + View = "decimal", + Name = "Maximum" + }); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs index 02324f7d84..a4dfd6c2ad 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownMultiplePropertyEditor.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.PropertyEditors [ParameterEditor("propertyTypePickerMultiple", "Name", "textbox")] [ParameterEditor("contentTypeMultiple", "Name", "textbox")] [ParameterEditor("tabPickerMultiple", "Name", "textbox")] - [PropertyEditor(Constants.PropertyEditors.DropDownListMultipleAlias, "Dropdown list multiple", "dropdown")] + [PropertyEditor(Constants.PropertyEditors.DropDownListMultipleAlias, "Dropdown list multiple", "dropdown", Group = "lists", Icon="icon-bulleted-list")] public class DropDownMultiplePropertyEditor : DropDownMultipleWithKeysPropertyEditor { protected override PropertyValueEditor CreateValueEditor() diff --git a/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs index b0a1c859d6..05ba3e2644 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownMultipleWithKeysPropertyEditor.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.PropertyEditors /// Due to maintaining backwards compatibility this data type stores the value as a string which is a comma separated value of the /// ids of the individual items so we have logic in here to deal with that. /// - [PropertyEditor(Constants.PropertyEditors.DropdownlistMultiplePublishKeysAlias, "Dropdown list multiple, publish keys", "dropdown")] + [PropertyEditor(Constants.PropertyEditors.DropdownlistMultiplePublishKeysAlias, "Dropdown list multiple, publish keys", "dropdown", Group = "lists", Icon = "icon-bulleted-list")] public class DropDownMultipleWithKeysPropertyEditor : DropDownPropertyEditor { protected override PropertyValueEditor CreateValueEditor() diff --git a/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs index 4cc8f068cf..115b99dea4 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownPropertyEditor.cs @@ -16,7 +16,7 @@ namespace Umbraco.Web.PropertyEditors /// as INT and we have logic in here to ensure it is formatted correctly including ensuring that the string value is published /// in cache and not the int ID. /// - [PropertyEditor(Constants.PropertyEditors.DropDownListAlias, "Dropdown list", "dropdown", ValueType = "STRING")] + [PropertyEditor(Constants.PropertyEditors.DropDownListAlias, "Dropdown list", "dropdown", ValueType = PropertyEditorValueTypes.String, Group = "lists", Icon = "icon-indent")] public class DropDownPropertyEditor : DropDownWithKeysPropertyEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs index f0d695a7eb..a33115003c 100644 --- a/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DropDownWithKeysPropertyEditor.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PropertyEditors /// as INT and we have logic in here to ensure it is formatted correctly including ensuring that the INT ID value is published /// in cache and not the string value. /// - [PropertyEditor(Constants.PropertyEditors.DropdownlistPublishingKeysAlias, "Dropdown list, publishing keys", "dropdown", ValueType = "INT")] + [PropertyEditor(Constants.PropertyEditors.DropdownlistPublishingKeysAlias, "Dropdown list, publishing keys", "dropdown", ValueType = PropertyEditorValueTypes.Integer, Group = "lists", Icon = "icon-indent")] public class DropDownWithKeysPropertyEditor : PropertyEditor { diff --git a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs index b4b702f935..8d141fd1e9 100644 --- a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.EmailAddressAlias, "Email address", "email")] + [PropertyEditor(Constants.PropertyEditors.EmailAddressAlias, "Email address", "email", Icon="icon-message")] public class EmailAddressPropertyEditor : PropertyEditor { protected override PropertyValueEditor CreateValueEditor() diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 89af8b8f66..3c30586383 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -20,33 +20,9 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.UploadFieldAlias, "File upload", "fileupload")] - public class FileUploadPropertyEditor : PropertyEditor + [PropertyEditor(Constants.PropertyEditors.UploadFieldAlias, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] + public class FileUploadPropertyEditor : PropertyEditor, IApplicationEventHandler { - /// - /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields - /// if we find any attached to the current media item. - /// - /// - /// I think this kind of logic belongs on this property editor, I guess it could exist elsewhere but it all has to do with the upload field. - /// - static FileUploadPropertyEditor() - { - MediaService.Saving += MediaServiceSaving; - MediaService.Created += MediaServiceCreating; - ContentService.Copied += ContentServiceCopied; - - MediaService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - MediaService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - ContentService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - MemberService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - } /// /// Creates our custom value editor @@ -275,5 +251,54 @@ namespace Umbraco.Web.PropertyEditors } } + #region Application event handler, used to bind to events on startup + + private readonly FileUploadPropertyEditorApplicationStartup _applicationStartup = new FileUploadPropertyEditorApplicationStartup(); + + /// + /// we're using a sub -class because this has the logic to prevent it from executing if the application is not configured + /// + private class FileUploadPropertyEditorApplicationStartup : ApplicationEventHandler + { + /// + /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields + /// if we find any attached to the current media item. + /// + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + MediaService.Saving += MediaServiceSaving; + MediaService.Created += MediaServiceCreating; + ContentService.Copied += ContentServiceCopied; + + MediaService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + MediaService.EmptiedRecycleBin += (sender, args) => + args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); + ContentService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + ContentService.EmptiedRecycleBin += (sender, args) => + args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); + MemberService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + } + } + + public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationInitialized(umbracoApplication, applicationContext); + } + public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationStarting(umbracoApplication, applicationContext); + } + public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationStarted(umbracoApplication, applicationContext); + } + #endregion + } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 8692744089..221dde865f 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -63,10 +63,10 @@ namespace Umbraco.Web.PropertyEditors clear = json["clearFiles"].Value(); } - var currentPersistedValues = new string[] {}; + var currentPersistedValues = new string[] { }; if (string.IsNullOrEmpty(currentValue.ToString()) == false) { - currentPersistedValues = currentValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + currentPersistedValues = currentValue.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); } var newValue = new List(); @@ -82,7 +82,7 @@ namespace Umbraco.Web.PropertyEditors } return ""; } - + //check for any files if (editorValue.AdditionalData.ContainsKey("files")) { @@ -131,14 +131,13 @@ namespace Umbraco.Web.PropertyEditors if (umbracoFile.SupportsResizing) { var additionalSizes = new List(); - //get the pre-vals value + //get the pre-vals value var thumbs = editorValue.PreValues.FormatAsDictionary(); if (thumbs.Any()) { var thumbnailSizes = thumbs.First().Value.Value; - // additional thumbnails configured as prevalues on the DataType - var sep = (thumbnailSizes.Contains("") == false && thumbnailSizes.Contains(",")) ? ',' : ';'; - foreach (var thumb in thumbnailSizes.Split(sep)) + // additional thumbnails configured as prevalues on the DataType + foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) { int thumbSize; if (thumb == "" || int.TryParse(thumb, out thumbSize) == false) continue; @@ -150,22 +149,22 @@ namespace Umbraco.Web.PropertyEditors { ImageHelper.GenerateMediaThumbnails(fs, fileName, umbracoFile.Extension, image, additionalSizes); } - } + newValue.Add(umbracoFile.Url); //add to the saved paths savedFilePaths.Add(umbracoFile.Url); } //now remove the temp file - File.Delete(file.TempFilePath); + File.Delete(file.TempFilePath); } - + //Remove any files that are no longer saved for this item foreach (var toRemove in currentPersistedValues.Except(savedFilePaths)) { fs.DeleteFile(fs.GetRelativePath(toRemove), true); } - + return string.Join(",", newValue); } diff --git a/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs index 7d13977dd3..bfdbe368d1 100644 --- a/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FolderBrowserPropertyEditor.cs @@ -1,15 +1,17 @@ -using System.ComponentModel; +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.FolderBrowserAlias, "Folder Browser", "folderbrowser", HideLabel=true)] + [Obsolete("This is no longer used by default, use the ListViewPropertyEditor instead")] + [PropertyEditor(Constants.PropertyEditors.FolderBrowserAlias, "(Obsolete) Folder Browser", "folderbrowser", HideLabel=true, Icon="icon-folder", Group="media")] public class FolderBrowserPropertyEditor : PropertyEditor { - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 060cbdb43c..5bb05cd6f6 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -3,15 +3,96 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Examine; +using Lucene.Net.Documents; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using UmbracoExamine; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Core.Constants.PropertyEditors.GridAlias, "Grid layout", "grid", HideLabel = true, IsParameterEditor = false, ValueType = "JSON")] - public class GridPropertyEditor : PropertyEditor - { + [PropertyEditor(Core.Constants.PropertyEditors.GridAlias, "Grid layout", "grid", HideLabel = true, IsParameterEditor = false, ValueType = PropertyEditorValueTypes.Json, Group="rich content", Icon="icon-layout")] + public class GridPropertyEditor : PropertyEditor, IApplicationEventHandler + { + + private static void DocumentWriting(object sender, Examine.LuceneEngine.DocumentWritingEventArgs e) + { + var indexer = (BaseUmbracoIndexer)sender; + foreach (var field in indexer.IndexerData.UserFields) + { + if (e.Fields.ContainsKey(field.Name)) + { + if (e.Fields[field.Name].DetectIsJson()) + { + try + { + var json = JsonConvert.DeserializeObject(e.Fields[field.Name]); + + //check if this is formatted for grid json + JToken name; + JToken sections; + if (json.HasValues && json.TryGetValue("name", out name) && json.TryGetValue("sections", out sections)) + { + //get all values and put them into a single field (using JsonPath) + var sb = new StringBuilder(); + foreach (var row in json.SelectTokens("$.sections[*].rows[*]")) + { + var rowName = row["name"].Value(); + var areaVals = row.SelectTokens("$.areas[*].controls[*].value"); + + foreach (var areaVal in areaVals) + { + //TODO: If it's not a string, then it's a json formatted value - + // we cannot really index this in a smart way since it could be 'anything' + if (areaVal.Type == JTokenType.String) + { + var str = areaVal.Value(); + str = XmlHelper.CouldItBeXml(str) ? str.StripHtml() : str; + sb.Append(str); + sb.Append(" "); + + //add the row name as an individual field + e.Document.Add( + new Field( + string.Format("{0}.{1}", field.Name, rowName), str, Field.Store.YES, Field.Index.ANALYZED)); + } + + } + } + + if (sb.Length > 0) + { + //First save the raw value to a raw field + e.Document.Add( + new Field( + string.Format("{0}{1}", UmbracoContentIndexer.RawFieldPrefix, field.Name), + e.Fields[field.Name], Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO)); + + //now replace the original value with the combined/cleaned value + e.Document.RemoveField(field.Name); + e.Document.Add( + new Field( + field.Name, + sb.ToString(), Field.Store.YES, Field.Index.ANALYZED)); + } + } + } + catch (JsonException) + { + //swallow...on purpose, there's a chance that this isn't json and we don't want that to affect + // the website. + } + + } + } + } + } + /// /// Overridden to ensure that the value is validated /// @@ -44,6 +125,44 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("rte", "Rich text editor", "views/propertyeditors/rte/rte.prevalues.html", Description = "Rich text editor configuration")] public string Rte { get; set; } } + + #region Application event handler, used to bind to events on startup + + private readonly GridPropertyEditorApplicationStartup _applicationStartup = new GridPropertyEditorApplicationStartup(); + + /// + /// we're using a sub -class because this has the logic to prevent it from executing if the application is not configured + /// + private class GridPropertyEditorApplicationStartup : ApplicationEventHandler + { + /// + /// We're going to bind to the Examine events so we can ensure grid data is index nicely. + /// + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + foreach (var i in ExamineManager.Instance.IndexProviderCollection.OfType()) + { + i.DocumentWriting += DocumentWriting; + } + } + } + + public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationInitialized(umbracoApplication, applicationContext); + } + public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationStarting(umbracoApplication, applicationContext); + } + public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationStarted(umbracoApplication, applicationContext); + } + #endregion } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 4acb783da6..14c267cf7d 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -15,35 +15,9 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.ImageCropperAlias, "Image Cropper", "imagecropper", ValueType = "JSON", HideLabel = false)] - public class ImageCropperPropertyEditor : PropertyEditor + [PropertyEditor(Constants.PropertyEditors.ImageCropperAlias, "Image Cropper", "imagecropper", ValueType = PropertyEditorValueTypes.Json, HideLabel = false, Group="media", Icon="icon-crop")] + public class ImageCropperPropertyEditor : PropertyEditor, IApplicationEventHandler { - - /// - /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields - /// if we find any attached to the current media item. - /// - /// - /// I think this kind of logic belongs on this property editor, I guess it could exist elsewhere but it all has to do with the cropper. - /// - static ImageCropperPropertyEditor() - { - MediaService.Saving += MediaServiceSaving; - MediaService.Created += MediaServiceCreated; - ContentService.Copied += ContentServiceCopied; - - MediaService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - MediaService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - ContentService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - MemberService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - } - /// /// Creates our custom value editor /// @@ -265,5 +239,54 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("crops", "Crop sizes", "views/propertyeditors/imagecropper/imagecropper.prevalues.html")] public string Crops { get; set; } } + + #region Application event handler, used to bind to events on startup + + private readonly FileUploadPropertyEditorApplicationStartup _applicationStartup = new FileUploadPropertyEditorApplicationStartup(); + + /// + /// we're using a sub -class because this has the logic to prevent it from executing if the application is not configured + /// + private class FileUploadPropertyEditorApplicationStartup : ApplicationEventHandler + { + /// + /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields + /// if we find any attached to the current media item. + /// + protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + MediaService.Saving += MediaServiceSaving; + MediaService.Created += MediaServiceCreated; + ContentService.Copied += ContentServiceCopied; + + MediaService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + MediaService.EmptiedRecycleBin += (sender, args) => + args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); + ContentService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + ContentService.EmptiedRecycleBin += (sender, args) => + args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); + MemberService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + } + } + + public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationInitialized(umbracoApplication, applicationContext); + } + public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationStarting(umbracoApplication, applicationContext); + } + public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) + { + //wrap + _applicationStartup.OnApplicationStarted(umbracoApplication, applicationContext); + } + #endregion } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 7c84ff7026..a32a764929 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -15,6 +15,8 @@ using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.PropertyEditors @@ -26,6 +28,23 @@ namespace Umbraco.Web.PropertyEditors } + /// + /// This is called to merge in the prevalue crops with the value that is saved - similar to the property value converter for the front-end + /// + + public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService) + { + var val = base.ConvertDbToEditor(property, propertyType, dataTypeService); + + var json = val as JObject; + if (json != null) + { + ImageCropperValueConverter.MergePreValues(json, dataTypeService, propertyType.DataTypeDefinitionId); + return json; + } + + return val; + } /// /// Overrides the deserialize value so that we can save the file accordingly diff --git a/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs index b587c2620e..026aaf115f 100644 --- a/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/IntegerPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.IntegerAlias, "Numeric", "integer", IsParameterEditor = true)] + [PropertyEditor(Constants.PropertyEditors.IntegerAlias, "Numeric", "integer", IsParameterEditor = true, ValueType = PropertyEditorValueTypes.IntegerAlternative)] public class IntegerPropertyEditor : PropertyEditor { /// @@ -16,14 +16,14 @@ namespace Umbraco.Web.PropertyEditors editor.Validators.Add(new IntegerValidator()); return editor; } - - protected override PreValueEditor CreatePreValueEditor() + + protected override PreValueEditor CreatePreValueEditor() { - return new IntegerPreValueEditor(); - } - - /// - /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. + return new IntegerPreValueEditor(); + } + + /// + /// A custom pre-value editor class to deal with the legacy way that the pre-value data is stored. /// internal class IntegerPreValueEditor : PreValueEditor { diff --git a/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs index 0e421f5b14..c5482695a3 100644 --- a/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/LabelPropertyEditor.cs @@ -1,17 +1,15 @@ using System.Collections.Generic; -using System.ComponentModel; using System.Linq; -using System.Web.Mvc; +using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.NoEditAlias, "Label", "readonlyvalue")] + [PropertyEditor(Constants.PropertyEditors.NoEditAlias, "Label", "readonlyvalue", Icon = "icon-readonly")] public class LabelPropertyEditor : PropertyEditor { - protected override PropertyValueEditor CreateValueEditor() { return new LabelPropertyValueEditor(base.CreateValueEditor()); @@ -21,7 +19,7 @@ namespace Umbraco.Web.PropertyEditors { return new LabelPreValueEditor(); } - + /// /// Custom value editor to mark it as readonly /// @@ -43,19 +41,26 @@ namespace Umbraco.Web.PropertyEditors internal class LabelPreValueEditor : PreValueEditor { + private const string LegacyPropertyEditorValuesKey = "values"; + public LabelPreValueEditor() { Fields.Add(new PreValueField() { HideLabel = true, View = "readonlykeyvalues", - Key = "values" + Key = LegacyPropertyEditorValuesKey }); + + ValueType = PropertyEditorValueTypes.String; } + [PreValueField(Constants.PropertyEditors.PreValueKeys.DataValueType, "Value type", "valuetype")] + public string ValueType { get; set; } + /// - /// Chuck all the values into one field so devs can see what is stored there - we want this in case we've converted a legacy proeprty editor over to a label - /// we should still show the pre-values stored for the data type. + /// Other than for the pre-value fields defined on this property editor, chuck all the values into one field so devs can see what is stored there. + /// We want this in case we've converted a legacy property editor over to a label as we should still show the pre-values stored for the data type. /// /// /// @@ -63,12 +68,81 @@ namespace Umbraco.Web.PropertyEditors public override IDictionary ConvertDbToEditor(IDictionary defaultPreVals, PreValueCollection persistedPreVals) { var existing = base.ConvertDbToEditor(defaultPreVals, persistedPreVals); - //convert to a list, easier to enumerate on the editor - var asList = existing.Select(e => new KeyValuePair(e.Key, e.Value)).ToList(); - var result = new Dictionary { { "values", asList } }; + + // Check for a saved value type. If not found set to default string type. + var valueType = PropertyEditorValueTypes.String; + if (existing.ContainsKey(Constants.PropertyEditors.PreValueKeys.DataValueType)) + { + valueType = (string)existing[Constants.PropertyEditors.PreValueKeys.DataValueType]; + } + + // Convert any other values from a legacy property editor to a list, easier to enumerate on the editor. + // Make sure to exclude values defined on the label property editor itself. + var asList = existing + .Select(e => new KeyValuePair(e.Key, e.Value)) + .Where(e => e.Key != Constants.PropertyEditors.PreValueKeys.DataValueType) + .ToList(); + + var result = new Dictionary { { Constants.PropertyEditors.PreValueKeys.DataValueType, valueType } }; + if (asList.Any()) + { + result.Add(LegacyPropertyEditorValuesKey, asList); + } + return result; } - } + /// + /// When saving we want to avoid saving an empty "legacy property editor values" field if there are none. + /// + /// + /// + /// + public override IDictionary ConvertEditorToDb(IDictionary editorValue, PreValueCollection currentValue) + { + // notes (from the PR): + // + // "All stemmed from the fact that even though the label property editor could have pre-values (from a legacy type), + // they couldn't up to now be edited and saved through the UI. Now that a "true" pre-value has been added, it can, + // which led to some odd behaviour. + // + // Firstly there would be a pre-value record saved for legacy values even if there aren't any (the key would exist + // but with no value). In that case I remove that pre-value so it's not saved(likely does no harm, but it's not + // necessary - we only need this legacy values pre-value record if there are any). + // + // Secondly if there are legacy values, I found on each save the JSON structure containing them would get repeatedly + // nested (an outer JSON wrapper would be added each time). So what I'm doing is if there are legacy pre-values, + // I'm converting what comes in "wrapped" like (below) into the legacy property editor values." + + if (editorValue.ContainsKey(LegacyPropertyEditorValuesKey)) + { + // If provided value contains an empty legacy property editor values, don't save it + if (editorValue[LegacyPropertyEditorValuesKey] == null) + { + editorValue.Remove(LegacyPropertyEditorValuesKey); + } + else + { + // If provided value contains legacy property editor values, unwrap the value to save so it doesn't get repeatedly nested on saves. + // This is a bit funky - but basically needing to parse out the original value from a JSON structure that is passed in + // looking like: + // Value = {[ + // { + // "Key": "values", + // "Value": { + // + // }} + // ]} + var values = editorValue[LegacyPropertyEditorValuesKey] as JArray; + if (values != null && values.Count == 1 && values.First.Values().Count() == 2) + { + editorValue[LegacyPropertyEditorValuesKey] = values.First.Values().Last(); + } + } + } + + return base.ConvertEditorToDb(editorValue, currentValue); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs index 09dfacc94f..0e1a0abf2d 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewPropertyEditor.cs @@ -1,15 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using Umbraco.Core; -using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.ListViewAlias, "List view", "listview", HideLabel = true)] + [PropertyEditor(Constants.PropertyEditors.ListViewAlias, "List view", "listview", HideLabel = true, Group = "lists", Icon = "icon-item-arrangement")] public class ListViewPropertyEditor : PropertyEditor { protected override PreValueEditor CreatePreValueEditor() @@ -24,6 +19,7 @@ namespace Umbraco.Web.PropertyEditors return new Dictionary { {"pageSize", "10"}, + {"displayAtTabNumber", "1"}, {"orderBy", "SortOrder"}, {"orderDirection", "asc"}, { @@ -33,14 +29,30 @@ namespace Umbraco.Web.PropertyEditors new {alias = "updateDate", header = "Last edited", isSystem = 1}, new {alias = "owner", header = "Created by", isSystem = 1} } - } + }, + { + "layouts", new[] + { + new {name = "List", path = "views/propertyeditors/listview/layouts/list/list.html", icon = "icon-list", isSystem = 1, selected = true}, + new {name = "Grid", path = "views/propertyeditors/listview/layouts/grid/grid.html", icon = "icon-thumbnails-small", isSystem = 1, selected = true} + } + }, + {"bulkActionPermissions", new + { + allowBulkPublish = true, + allowBulkUnpublish = true, + allowBulkCopy = true, + allowBulkMove = true, + allowBulkDelete = true + }} }; } } internal class ListViewPreValueEditor : PreValueEditor { - + [PreValueField("displayAtTabNumber", "Display At Tab Number", "number", Description = "Which tab position that the list of child items will be displayed")] + public int DisplayAtTabNumber { get; set; } [PreValueField("pageSize", "Page Size", "number", Description = "Number of items per page")] public int PageSize { get; set; } @@ -51,11 +63,27 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("orderDirection", "Order Direction", "views/propertyeditors/listview/orderdirection.prevalues.html")] public int OrderDirection { get; set; } + [PreValueField("layouts", "Layouts", "views/propertyeditors/listview/layouts.prevalues.html")] + public int Layouts { get; set; } + [PreValueField("includeProperties", "Columns Displayed", "views/propertyeditors/listview/includeproperties.prevalues.html", Description = "The properties that will be displayed for each column")] public object IncludeProperties { get; set; } + [PreValueField("bulkActionPermissions", "Bulk Action Permissions", "views/propertyeditors/listview/bulkactionpermissions.prevalues.html", + Description = "The bulk actions that are allowed from the list view")] + public BulkActionPermissionSettings BulkActionPermissions { get; set; } + internal class BulkActionPermissionSettings + { + public bool AllowBulkPublish { get; set; } + + public bool AllowBulkUnpublish { get; set; } + + public bool AllowBulkCopy { get; set; } + + public bool AllowBulkMove { get; set; } + + public bool AllowBulkDelete { get; set; } + } } - - } } diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs index b8f328f7fa..3ac5788dd2 100644 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs @@ -9,7 +9,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MacroContainerAlias, "Macro container", "macrocontainer", ValueType = "TEXT")] + [PropertyEditor(Constants.PropertyEditors.MacroContainerAlias, "Macro container", "macrocontainer", ValueType = PropertyEditorValueTypes.Text, Group="rich content", Icon="icon-settings-alt")] public class MacroContainerPropertyEditor : PropertyEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs index bc9258c5e0..9fa8f2d588 100644 --- a/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MarkdownPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MarkdownEditorAlias, "Markdown editor", "markdowneditor", ValueType = "TEXT")] + [PropertyEditor(Constants.PropertyEditors.MarkdownEditorAlias, "Markdown editor", "markdowneditor", ValueType = PropertyEditorValueTypes.Text, Icon="icon-code", Group="rich content")] public class MarkdownPropertyEditor : PropertyEditor { protected override PreValueEditor CreatePreValueEditor() diff --git a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs index 947c9f0062..7e966d31ad 100644 --- a/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MediaPickerPropertyEditor.cs @@ -9,14 +9,15 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MediaPickerAlias, "Legacy Media Picker", "INT", "mediapicker")] + [PropertyEditor(Constants.PropertyEditors.MediaPickerAlias, "Legacy Media Picker", PropertyEditorValueTypes.Integer, "mediapicker", Group="media", Icon="icon-picture")] public class MediaPickerPropertyEditor : PropertyEditor { public MediaPickerPropertyEditor() { InternalPreValues = new Dictionary { - {"multiPicker", "0"} + {"multiPicker", "0"}, + {"onlyImages", "0"} }; } diff --git a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs index 92f7c106d9..433199c536 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -8,7 +8,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MemberGroupPickerAlias, "Member Group Picker", "membergrouppicker")] + [PropertyEditor(Constants.PropertyEditors.MemberGroupPickerAlias, "Member Group Picker", "membergrouppicker", Group="People", Icon="icon-users")] public class MemberGroupPickerPropertyEditor : PropertyEditor { } diff --git a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs index a1c99b83a5..7dadbfe24c 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberPickerPropertyEditor.cs @@ -8,7 +8,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MemberPickerAlias, "Member Picker", "INT", "memberpicker")] + [PropertyEditor(Constants.PropertyEditors.MemberPickerAlias, "Member Picker", PropertyEditorValueTypes.Integer, "memberpicker", Group = "People", Icon = "icon-user")] public class MemberPickerPropertyEditor : PropertyEditor { } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index 673e721b70..b3d3f02448 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -1,24 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MultiNodeTreePickerAlias, "Multinode Treepicker", "contentpicker")] + [PropertyEditor(Constants.PropertyEditors.MultiNodeTreePickerAlias, "Multinode Treepicker", "contentpicker", Group="pickers", Icon="icon-page-add")] public class MultiNodeTreePickerPropertyEditor : PropertyEditor { public MultiNodeTreePickerPropertyEditor() { _internalPreValues = new Dictionary - { - {"multiPicker", "1"}, - {"showEditButton", "0"} - }; + { + {"multiPicker", "1"}, + {"showOpenButton", "0"}, + {"showEditButton", "0"}, + {"showPathOnHover", "0"} + }; } protected override PreValueEditor CreatePreValueEditor() @@ -47,9 +45,16 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("maxNumber", "Maximum number of items", "number")] public string MaxNumber { get; set; } + + [PreValueField("showOpenButton", "Show open button", "boolean")] + public string ShowOpenButton { get; set; } + [PreValueField("showEditButton", "Show edit button (this feature is in preview!)", "boolean")] public string ShowEditButton { get; set; } + [PreValueField("showPathOnHover", "Show path when hovering items", "boolean")] + public bool ShowPathOnHover { get; set; } + /// /// This ensures the multiPicker pre-val is set based on the maxNumber of nodes set /// diff --git a/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs index 1ce77804fa..37587d1c54 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleMediaPickerPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MultipleMediaPickerAlias, "Media Picker", "mediapicker")] + [PropertyEditor(Constants.PropertyEditors.MultipleMediaPickerAlias, "Media Picker", "mediapicker", Group = "media", Icon = "icon-pictures-alt-2")] public class MultipleMediaPickerPropertyEditor : MediaPickerPropertyEditor { public MultipleMediaPickerPropertyEditor() @@ -22,6 +22,12 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("multiPicker", "Pick multiple items", "boolean")] public bool MultiPicker { get; set; } + [PreValueField("onlyImages", "Pick only images", "boolean", Description = "Only let the editor choose images from media.")] + public bool OnlyImages { get; set; } + + [PreValueField("disableFolderSelect", "Disable folder select", "boolean", Description = "Do not allow folders to be picked.")] + public bool DisableFolderSelect { get; set; } + [PreValueField("startNodeId", "Start node", "mediapicker")] public int StartNodeId { get; set; } } diff --git a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs index cc6c0468a3..96288c9857 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -14,7 +14,7 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.MultipleTextstringAlias, "Multiple Textbox", "multipletextbox", ValueType = "TEXT")] + [PropertyEditor(Constants.PropertyEditors.MultipleTextstringAlias, "Repeatable textstrings", "multipletextbox", ValueType = PropertyEditorValueTypes.Text, Icon="icon-ordered-list", Group="lists")] public class MultipleTextStringPropertyEditor : PropertyEditor { protected override PropertyValueEditor CreateValueEditor() diff --git a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs index eb482e714e..2bcd8fcbd8 100644 --- a/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RadioButtonsPropertyEditor.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PropertyEditors /// as INT and we have logic in here to ensure it is formatted correctly including ensuring that the INT ID value is published /// in cache and not the string value. /// - [PropertyEditor(Constants.PropertyEditors.RadioButtonListAlias, "Radio button list", "radiobuttons", ValueType = "INT")] + [PropertyEditor(Constants.PropertyEditors.RadioButtonListAlias, "Radio button list", "radiobuttons", ValueType = PropertyEditorValueTypes.Integer, Group="lists", Icon="icon-target")] public class RadioButtonsPropertyEditor : DropDownWithKeysPropertyEditor { diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs index cbc332d66e..cf9d4100a4 100644 --- a/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs @@ -8,8 +8,18 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.RelatedLinksAlias, "Related links", "relatedlinks", ValueType ="JSON")] + [PropertyEditor(Constants.PropertyEditors.RelatedLinksAlias, "Related links", "relatedlinks", ValueType = PropertyEditorValueTypes.Json, Icon="icon-thumbnail-list", Group="pickers")] public class RelatedLinksPropertyEditor : PropertyEditor { + protected override PreValueEditor CreatePreValueEditor() + { + return new RelatedLinksPreValueEditor(); + } + + internal class RelatedLinksPreValueEditor : PreValueEditor + { + [PreValueField("max", "Maximum number of links", "number", Description = "Enter the maximum amount of links to be added, enter 0 for unlimited")] + public int Maximum { get; set; } + } } } diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPreValueController.cs b/src/Umbraco.Web/PropertyEditors/RichTextPreValueController.cs index 9622008a05..ca8df2b9be 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPreValueController.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPreValueController.cs @@ -42,8 +42,6 @@ namespace Umbraco.Web.PropertyEditors return config; } - - private static void EnsureInit() { @@ -71,9 +69,10 @@ namespace Umbraco.Web.PropertyEditors new RichTextEditorCommand() { IsStylePicker = isStyle, + Name = n.SelectSingleNode("./name") != null ? n.SelectSingleNode("./name").FirstChild.Value : alias, Icon = n.SelectSingleNode("./icon").FirstChild.Value, Command = n.SelectSingleNode("./tinyMceCommand").FirstChild.Value, - Alias = n.SelectSingleNode("./umbracoAlias").FirstChild.Value.ToLower(), + Alias = alias, UserInterface = n.SelectSingleNode("./tinyMceCommand").Attributes.GetNamedItem("userInterface").Value, FrontEndCommand = n.SelectSingleNode("./tinyMceCommand").Attributes.GetNamedItem("frontendCommand").Value, Value = n.SelectSingleNode("./tinyMceCommand").Attributes.GetNamedItem("value").Value, @@ -136,4 +135,4 @@ namespace Umbraco.Web.PropertyEditors } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 6610c0e12c..de65a326f7 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.TinyMCEAlias, "Rich Text Editor", "rte", ValueType = "TEXT", HideLabel = false)] + [PropertyEditor(Constants.PropertyEditors.TinyMCEAlias, "Rich Text Editor", "rte", ValueType = PropertyEditorValueTypes.Text, HideLabel = false, Group="Rich Content", Icon="icon-browser-window")] public class RichTextPropertyEditor : PropertyEditor { /// diff --git a/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs index 509739c354..c9a08de894 100644 --- a/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/SliderPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.SliderAlias, "Slider", "slider")] + [PropertyEditor(Constants.PropertyEditors.SliderAlias, "Slider", "slider", Icon="icon-navigation-horizontal")] public class SliderPropertyEditor : PropertyEditor { protected override PreValueEditor CreatePreValueEditor() @@ -15,7 +15,10 @@ namespace Umbraco.Web.PropertyEditors { [PreValueField("enableRange", "Enable range", "boolean")] - public string Type { get; set; } + public string Range { get; set; } + + [PreValueField("orientation", "Orientation", "views/propertyeditors/slider/orientation.prevalues.html")] + public string Orientation { get; set; } [PreValueField("initVal1", "Initial value", "number")] public int InitialValue { get; set; } @@ -32,9 +35,38 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("step", "Step increments", "number")] public int StepIncrements { get; set; } - [PreValueField("orientation", "Orientation", "views/propertyeditors/slider/orientation.prevalues.html")] - public string Orientation { get; set; } + [PreValueField("precision", "Precision", "number", Description = "The number of digits shown after the decimal. Defaults to the number of digits after the decimal of step value.")] + public int Precision { get; set; } + [PreValueField("handle", "Handle", "views/propertyeditors/slider/handle.prevalues.html", Description = "Handle shape. Default is 'round\'")] + public string Handle { get; set; } + + [PreValueField("tooltip", "Tooltip", "views/propertyeditors/slider/tooltip.prevalues.html", Description = "Whether to show the tooltip on drag, hide the tooltip, or always show the tooltip. Accepts: 'show', 'hide', or 'always'")] + public string Tooltip { get; set; } + + [PreValueField("tooltipSplit", "Tooltip split", "boolean", Description = "If false show one tootip if true show two tooltips one for each handler")] + public string TooltipSplit { get; set; } + + [PreValueField("tooltipFormat", "Tooltip format", "textstring", Description = "The value wanted to be displayed in the tooltip. Use {0} and {1} for current values - {1} is only for range slider and if not using tooltip split.")] + public string TooltipFormat { get; set; } + + [PreValueField("tooltipPosition", "Tooltip position", "textstring", Description = "Position of tooltip, relative to slider. Accepts 'top'/'bottom' for horizontal sliders and 'left'/'right' for vertically orientated sliders. Default positions are 'top' for horizontal and 'right' for vertical slider.")] + public string TooltipPosition { get; set; } + + [PreValueField("reversed", "Reversed", "boolean", Description = "Whether or not the slider should be reversed")] + public string Reversed { get; set; } + + [PreValueField("ticks", "Ticks", "textstring", Description = "Comma-separated values. Used to define the values of ticks. Tick marks are indicators to denote special values in the range. This option overwrites min and max options.")] + public string Ticks { get; set; } + + [PreValueField("ticksPositions", "Ticks positions", "textstring", Description = "Comma-separated values. Defines the positions of the tick values in percentages. The first value should always be 0, the last value should always be 100 percent.")] + public string TicksPositions { get; set; } + + [PreValueField("ticksLabels", "Ticks labels", "textstring", Description = "Comma-separated values. Defines the labels below the tick marks. Accepts HTML input.")] + public string TicksLabels { get; set; } + + [PreValueField("ticksSnapBounds", "Ticks snap bounds", "number", Description = "Used to define the snap bounds of a tick. Snaps to the tick if value is within these bounds.")] + public string TicksSnapBounds { get; set; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 3f709bb399..b44c65b157 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -11,7 +11,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { [SupportTags(typeof(TagPropertyEditorTagDefinition), ValueType = TagValueType.CustomTagList)] - [PropertyEditor(Constants.PropertyEditors.TagsAlias, "Tags", "tags")] + [PropertyEditor(Constants.PropertyEditors.TagsAlias, "Tags", "tags", Icon="icon-tags")] public class TagsPropertyEditor : PropertyEditor { public TagsPropertyEditor() diff --git a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs index 45a1f24aec..3bec23f004 100644 --- a/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextAreaPropertyEditor.cs @@ -3,7 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.TextboxMultipleAlias, "Textarea", "textarea", IsParameterEditor = true, ValueType = "TEXT")] + [PropertyEditor(Constants.PropertyEditors.TextboxMultipleAlias, "Textarea", "textarea", IsParameterEditor = true, ValueType = PropertyEditorValueTypes.Text, Icon="icon-application-window-alt")] public class TextAreaPropertyEditor : PropertyEditor { } diff --git a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs index 69bc1452cf..bcfbbbf682 100644 --- a/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TextboxPropertyEditor.cs @@ -10,7 +10,7 @@ using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.TextboxAlias, "Textbox", "textbox", IsParameterEditor = true)] + [PropertyEditor(Constants.PropertyEditors.TextboxAlias, "Textbox", "textbox", IsParameterEditor = true, Group = "Common")] public class TextboxPropertyEditor : PropertyEditor { } diff --git a/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs index 94561e5858..0dc181cbc3 100644 --- a/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TrueFalsePropertyEditor.cs @@ -3,8 +3,18 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.TrueFalseAlias, "True/False", "INT", "boolean", IsParameterEditor = true)] + [PropertyEditor(Constants.PropertyEditors.TrueFalseAlias, "True/False", PropertyEditorValueTypes.Integer, "boolean", IsParameterEditor = true, Group = "Common", Icon="icon-checkbox")] public class TrueFalsePropertyEditor : PropertyEditor { + protected override PreValueEditor CreatePreValueEditor() + { + return new TrueFalsePreValueEditor(); + } + + internal class TrueFalsePreValueEditor : PreValueEditor + { + [PreValueField("default", "Default Value", "boolean")] + public string Default { get; set; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs index 830fdec6a7..e4a070cb29 100644 --- a/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/UserPickerPropertyEditor.cs @@ -6,7 +6,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.UserPickerAlias, "User picker", "INT", "entitypicker")] + [PropertyEditor(Constants.PropertyEditors.UserPickerAlias, "User picker", PropertyEditorValueTypes.Integer, "entitypicker", Group="People", Icon="icon-user")] public class UserPickerPropertyEditor : PropertyEditor { private IDictionary _defaultPreValues; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs new file mode 100644 index 0000000000..82278676cd --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropDataSetConverter.cs @@ -0,0 +1,46 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// Used to do some type conversions from ImageCropDataSet to string and JObject + /// + public class ImageCropDataSetConverter : TypeConverter + { + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + var convertableTypes = new[] + { + typeof(JObject) + }; + + return convertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType)) + || base.CanConvertFrom(context, destinationType); + } + + public override object ConvertTo( + ITypeDescriptorContext context, + CultureInfo culture, + object value, + Type destinationType) + { + var cropDataSet = value as ImageCropDataSet; + if (cropDataSet == null) + return null; + + //JObject + if (TypeHelper.IsTypeAssignableFrom(destinationType)) + { + return JObject.FromObject(cropDataSet); + } + + return base.ConvertTo(context, culture, value, destinationType); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs new file mode 100644 index 0000000000..c959d26721 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -0,0 +1,47 @@ +using System.Globalization; +using System.Xml; +using System.Xml.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Dynamics; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + /// + /// Used to strongly type the value for the image cropper + /// + [DefaultPropertyValueConverter(typeof (JsonValueConverter))] //this shadows the JsonValueConverter + [PropertyValueType(typeof (ImageCropDataSet))] + [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Content)] + public class ImageCropperValueConverter : Core.PropertyEditors.ValueConverters.ImageCropperValueConverter + { + public ImageCropperValueConverter() + { + } + + public ImageCropperValueConverter(IDataTypeService dataTypeService) : base(dataTypeService) + { + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + var baseVal = base.ConvertDataToSource(propertyType, source, preview); + var json = baseVal as JObject; + if (json == null) return baseVal; + + var serializer = new JsonSerializer + { + Culture = CultureInfo.InvariantCulture, + FloatParseHandling = FloatParseHandling.Decimal + }; + + //return the strongly typed model + return json.ToObject(serializer); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index af66278815..681aa2ee72 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -1,4 +1,5 @@ -using System; +using MarkdownSharp; +using System; using System.Web; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; @@ -31,8 +32,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) { + // Convert markup to html for frontend rendering. + Markdown mark = new Markdown(); + // source should come from ConvertSource and be a string (or null) already - return new HtmlString(source == null ? string.Empty : (string)source); + return new HtmlString(source == null ? string.Empty : mark.Transform((string)source)); } public override object ConvertSourceToXPath(PublishedPropertyType propertyType, object source, bool preview) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs index 2278b18b59..8938325c35 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/TextStringValueConverter.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Web.Templates; namespace Umbraco.Web.PropertyEditors.ValueConverters @@ -10,9 +14,15 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [PropertyValueCache(PropertyCacheValue.All, PropertyCacheLevel.Request)] public class TextStringValueConverter : PropertyValueConverterBase { + private readonly static string[] PropertyTypeAliases = + { + Constants.PropertyEditors.TextboxAlias, + Constants.PropertyEditors.TextboxMultipleAlias + }; + public override bool IsConverter(PublishedPropertyType propertyType) { - return Constants.PropertyEditors.TextboxAlias.Equals(propertyType.PropertyEditorAlias); + return PropertyTypeAliases.Contains(propertyType.PropertyEditorAlias); } public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 71c27f2bee..8e9b529b7f 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -55,19 +55,28 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // cache if we have a content and not previewing if (content != null && preview == false) - { - var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/'))); - var iscanon = - UnitTesting == false - && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false; - // and only if this is the canonical url (the one GetUrl would return) - if (iscanon) - _routesCache.Store(contentId, route); - } + AddToCacheIfDeepestRoute(umbracoContext, content, route); return content; } + private void AddToCacheIfDeepestRoute(UmbracoContext umbracoContext, IPublishedContent content, string route) + { + var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/'))); + + // so we have a route that maps to a content... say "1234/path/to/content" - however, there could be a + // domain set on "to" and route "4567/content" would also map to the same content - and due to how + // urls computing work (by walking the tree up to the first domain we find) it is that second route + // that would be returned - the "deepest" route - and that is the route we want to cache, *not* the + // longer one - so make sure we don't cache the wrong route + + var deepest = UnitTesting == false + && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false; + + if (deepest) + _routesCache.Store(content.Id, route); + } + public virtual string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId) { // try to get from cache if not previewing @@ -80,11 +89,35 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // else actually determine the route route = DetermineRouteById(umbracoContext, preview, contentId); - // cache if we have a route and not previewing - if (route != null && preview == false) + // node not found + if (route == null) + return null; + + // find the content back, detect routes collisions: we should find ourselves back, + // else it means that another content with "higher priority" is sharing the same route. + // perf impact: + // - non-colliding, adds one complete "by route" lookup, only on the first time a url is computed (then it's cached anyways) + // - colliding, adds one "by route" lookup, the first time the url is computed, then one dictionary looked each time it is computed again + // assuming no collisions, the impact is one complete "by route" lookup the first time each url is computed + var loopId = preview ? 0 : _routesCache.GetNodeId(route); // might be cached already in case of collision + if (loopId == 0) + { + var content = DetermineIdByRoute(umbracoContext, preview, route, GlobalSettings.HideTopLevelNodeFromPath); + + // add the other route to cache so next time we have it already + if (content != null && preview == false) + AddToCacheIfDeepestRoute(umbracoContext, content, route); + + loopId = content == null ? 0 : content.Id; // though... 0 here would be quite weird? + } + + // cache if we have a route and not previewing and it's not a colliding route + // (the result of DetermineRouteById is always the deepest route) + if (/*route != null &&*/ preview == false && loopId == contentId) _routesCache.Store(contentId, route); - return route; + // return route if no collision, else report collision + return loopId == contentId ? route : ("err/" + loopId); } IPublishedContent DetermineIdByRoute(UmbracoContext umbracoContext, bool preview, string route, bool hideTopLevelNode) @@ -159,7 +192,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // we add this check - we look for the document matching "/" and if it's not us, then // we do not hide the top level path // it has to be taken care of in GetByRoute too so if - // "/foo" fails (looking for "/*/foo") we try also "/foo". + // "/foo" fails (looking for "/*/foo") we try also "/foo". // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but // that's the way it works pre-4.10 and we try to be backward compat for the time being if (node.Parent == null) @@ -244,8 +277,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private static IPublishedContent ConvertToDocument(XmlNode xmlNode, bool isPreviewing) { - return xmlNode == null - ? null + return xmlNode == null + ? null : (new XmlPublishedContent(xmlNode, isPreviewing)).CreateModel(); } @@ -398,7 +431,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (startNodeId > 0) { // if in a domain then use the root node of the domain - xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); + xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); } else { @@ -409,7 +442,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // umbraco does not consistently guarantee that sortOrder starts with 0 // so the one that we want is the one with the smallest sortOrder // read http://stackoverflow.com/questions/1128745/how-can-i-use-xpath-to-find-the-minimum-value-of-an-attribute-in-a-set-of-elemen - + // so that one does not work, because min(@sortOrder) maybe 1 // xpath = "/root/*[@isDoc and @sortOrder='0']"; @@ -453,7 +486,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache else { xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part); - + } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index 61dd50e610..79806b3273 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -87,7 +87,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { throw new NotImplementedException("PublishedMediaCache does not support XPath."); } - + public virtual IEnumerable GetByXPath(UmbracoContext umbracoContext, bool preview, string xpath, XPathVariable[] vars) { throw new NotImplementedException("PublishedMediaCache does not support XPath."); @@ -105,7 +105,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public bool XPathNavigatorIsNavigable { get { return false; } } - public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); } + public virtual bool HasContent(UmbracoContext context, bool preview) { throw new NotImplementedException(); } private ExamineManager GetExamineManagerSafe() { @@ -138,7 +138,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache return indexer; } catch (Exception ex) - { + { LogHelper.Error("Could not retrieve the InternalIndexer", ex); //something didn't work, continue returning null. } @@ -182,6 +182,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // it is called, but at least it should NOT hit the database // nor Lucene each time, relying on the memory cache instead + if (id <= 0) return null; // fail fast + var cacheValues = GetCacheValues(id, GetUmbracoMediaCacheValues); return cacheValues == null ? null : CreateFromCacheValues(cacheValues); @@ -197,11 +199,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { //first check in Examine as this is WAY faster var criteria = searchProvider.CreateSearchCriteria("media"); - + var filter = criteria.Id(id).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. //+(+__NodeId:3113 -__Path:-1,-21,*) +__IndexType:media - + var results = searchProvider.Search(filter.Compile()); if (results.Any()) { @@ -215,7 +217,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //Catch the exception here for the time being, and just fallback to GetMedia //TODO: Need to fix examine in LB scenarios! LogHelper.Error("Could not load data from Examine index for media", ex); - } + } } LogHelper.Warn( @@ -231,8 +233,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (media != null && media.Current != null) { - return media.Current.Name.InvariantEquals("error") - ? null + return media.Current.Name.InvariantEquals("error") + ? null : ConvertFromXPathNavigator(media.Current); } @@ -245,14 +247,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache internal CacheValues ConvertFromSearchResult(SearchResult searchResult) { - //NOTE: Some fields will not be included if the config section for the internal index has been + //NOTE: Some fields will not be included if the config section for the internal index has been //mucked around with. It should index everything and so the index definition should simply be: // - + var values = new Dictionary(searchResult.Fields); //we need to ensure some fields exist, because of the above issue - if (!new []{"template", "templateId"}.Any(values.ContainsKey)) + if (!new []{"template", "templateId"}.Any(values.ContainsKey)) values.Add("template", 0.ToString()); if (!new[] { "sortOrder" }.Any(values.ContainsKey)) values.Add("sortOrder", 0.ToString()); @@ -269,7 +271,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (!new[] { "createDate" }.Any(values.ContainsKey)) values.Add("createDate", default(DateTime).ToString("yyyy-MM-dd HH:mm:ss")); if (!new[] { "level" }.Any(values.ContainsKey)) - { + { values.Add("level", values["__Path"].Split(',').Length.ToString()); } @@ -303,7 +305,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { values["nodeTypeAlias"] = xpath.Name; } - + var result = xpath.SelectChildren(XPathNodeType.Element); //add the attributes e.g. id, parentId etc if (result.Current != null && result.Current.HasAttributes) @@ -320,7 +322,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (!values.ContainsKey(result.Current.Name)) { values[result.Current.Name] = result.Current.Value; - } + } } result.Current.MoveToParent(); } @@ -351,9 +353,9 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache XPath = forceNav ? xpath : null // outside of tests we do NOT want to cache the navigator! }; - //var content = new DictionaryPublishedContent(values, + //var content = new DictionaryPublishedContent(values, // d => d.ParentId != -1 //parent should be null if -1 - // ? GetUmbracoMedia(d.ParentId) + // ? GetUmbracoMedia(d.ParentId) // : null, // //callback to return the children of the current node based on the xml structure already found // d => GetChildrenMedia(d.Id, xpath), @@ -363,8 +365,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } /// - /// We will need to first check if the document was loaded by Examine, if so we'll need to check if this property exists - /// in the results, if it does not, then we'll have to revert to looking up in the db. + /// We will need to first check if the document was loaded by Examine, if so we'll need to check if this property exists + /// in the results, if it does not, then we'll have to revert to looking up in the db. /// /// /// @@ -399,7 +401,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// /// private IEnumerable GetChildrenMedia(int parentId, XPathNavigator xpath = null) - { + { //if there is no navigator, try examine first, then re-look it up if (xpath == null) @@ -412,7 +414,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { //first check in Examine as this is WAY faster var criteria = searchProvider.CreateSearchCriteria("media"); - + var filter = criteria.ParentId(parentId).Not().Field(UmbracoContentIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media @@ -433,7 +435,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { results = searchProvider.Search(filter.Compile()); } - + if (results.Any()) { // var medias = results.Select(ConvertFromSearchResult); @@ -451,7 +453,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache else { //if there's no result then return null. Previously we defaulted back to library.GetMedia below - //but this will always get called for when we are getting descendents since many items won't have + //but this will always get called for when we are getting descendents since many items won't have //children and then we are hitting the database again! //So instead we're going to rely on Examine to have the correct results like it should. return Enumerable.Empty(); @@ -462,7 +464,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco //See this thread: http://examine.cdodeplex.com/discussions/264341 //Catch the exception here for the time being, and just fallback to GetMedia - } + } } //falling back to get media @@ -514,13 +516,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // // will leave it here as it must have done something! // if (x.Name != "contents") // { - // //make sure it's actually a node, not a property + // //make sure it's actually a node, not a property // if (!string.IsNullOrEmpty(x.GetAttribute("path", "")) && // !string.IsNullOrEmpty(x.GetAttribute("id", ""))) // { // mediaList.Add(ConvertFromXPathNavigator(x)); // } - // } + // } //} return mediaList; @@ -530,7 +532,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// An IPublishedContent that is represented all by a dictionary. /// /// - /// This is a helper class and definitely not intended for public use, it expects that all of the values required + /// This is a helper class and definitely not intended for public use, it expects that all of the values required /// to create an IPublishedContent exist in the dictionary by specific aliases. /// internal class DictionaryPublishedContent : PublishedContentWithKeyBase @@ -544,7 +546,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private static readonly string[] IgnoredKeys = { "version", "isDoc" }; public DictionaryPublishedContent( - IDictionary valueDictionary, + IDictionary valueDictionary, Func getParent, Func> getChildren, Func getProperty, @@ -585,7 +587,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (int.TryParse(val, out pId)) { ParentId = pId; - } + } }, "parentID"); _contentType = PublishedContentType.Get(PublishedItemType.Media, _documentTypeAlias); @@ -600,7 +602,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache string value; const bool isPreviewing = false; // false :: never preview a media var property = valueDictionary.TryGetValue(alias, out value) == false - ? new XmlPublishedProperty(propertyType, isPreviewing) + ? new XmlPublishedProperty(propertyType, isPreviewing) : new XmlPublishedProperty(propertyType, isPreviewing, value); _properties.Add(property); } @@ -648,7 +650,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache internal bool LoadedFromExamine { get; private set; } //private readonly Func _getParent; - private readonly Lazy _getParent; + private readonly Lazy _getParent; //private readonly Func> _getChildren; private readonly Lazy> _getChildren; private readonly Func _getProperty; @@ -788,7 +790,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache IPublishedProperty property; string key = null; var cache = UmbracoContextCache.Current; - + if (cache != null) { key = string.Format("RECURSIVE_PROPERTY::{0}::{1}", Id, alias.ToLowerInvariant()); @@ -911,15 +913,15 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // we do clear a lot of things... but the cache refresher is somewhat // convoluted and it's hard to tell what to clear exactly ;-( - + // clear the parent - NOT (why?) //var exist = (CacheValues) cache.GetCacheItem(key); //if (exist != null) // cache.ClearCacheItem(PublishedMediaCacheKey + GetValuesValue(exist.Values, "parentID")); - + // clear the item cache.ClearCacheItem(key); - + // clear all children - in case we moved and their path has changed var fid = "/" + sid + "/"; cache.ClearCacheObjectTypes((k, v) => diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 3fa11a0dc2..bdf02ba5ca 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -148,34 +148,14 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public override async Task RunAsync(CancellationToken token) + public override Task RunAsync(CancellationToken token) { - lock (_locko) - { - _logger.Logger.Debug("Run now (async)."); - // just make sure - in case the runner is running the task on shutdown - _released = true; - } - - // http://stackoverflow.com/questions/13489065/best-practice-to-call-configureawait-for-all-server-side-code - // http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html - // do we really need that ConfigureAwait here? - - // - In theory, no, because we are already executing on a background thread because we know it is there and - // there won't be any SynchronizationContext to resume to, however this is 'library' code and - // who are we to say that this will never be executed in a sync context... this is best practice to be sure - // it won't cause problems. - // .... so yes we want it. - - using (await _runLock.LockAsync()) - { - await _content.SaveXmlToFileAsync().ConfigureAwait(false); - } + throw new NotImplementedException(); } public override bool IsAsync { - get { return true; } + get { return false; } } public override void Run() diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d6cf3b3141..578aab2755 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -116,6 +116,21 @@ namespace Umbraco.Web #endregion + #region IsComposedOf + + /// + /// Gets a value indicating whether the content is of a content type composed of the given alias + /// + /// The content. + /// The content type alias. + /// A value indicating whether the content is of a content type composed of a content type identified by the alias. + public static bool IsComposedOf(this IPublishedContent content, string alias) + { + return content.ContentType.CompositionAliases.Contains(alias); + } + + #endregion + #region HasProperty /// @@ -1186,6 +1201,7 @@ namespace Umbraco.Web /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). internal static IEnumerable EnumerateAncestors(this IPublishedContent content, bool orSelf) { + if (content == null) throw new ArgumentNullException("content"); if (orSelf) yield return content; while ((content = content.Parent) != null) yield return content; @@ -1358,6 +1374,7 @@ namespace Umbraco.Web internal static IEnumerable EnumerateDescendants(this IPublishedContent content, bool orSelf) { + if (content == null) throw new ArgumentNullException("content"); if (orSelf) yield return content; foreach (var child in content.Children) @@ -1692,6 +1709,7 @@ namespace Umbraco.Web public static T Parent(this IPublishedContent content) where T : class, IPublishedContent { + if (content == null) throw new ArgumentNullException("content"); return content.Parent as T; } @@ -1709,9 +1727,10 @@ namespace Umbraco.Web /// This method exists for consistency, it is the same as calling content.Children as a property. /// public static IEnumerable Children(this IPublishedContent content) - { - return content.Children; - } + { + if (content == null) throw new ArgumentNullException("content"); + return content.Children; + } /// /// Gets the children of the content, filtered by a predicate. diff --git a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs index ec3a8b2442..26ac3bd5df 100644 --- a/src/Umbraco.Web/RequestLifespanMessagesFactory.cs +++ b/src/Umbraco.Web/RequestLifespanMessagesFactory.cs @@ -8,21 +8,21 @@ namespace Umbraco.Web /// internal class RequestLifespanMessagesFactory : IEventMessagesFactory { - private readonly IUmbracoContextAccessor _ctxAccessor; + private readonly IHttpContextAccessor _httpAccessor; - public RequestLifespanMessagesFactory(IUmbracoContextAccessor ctxAccessor) + public RequestLifespanMessagesFactory(IHttpContextAccessor httpAccessor) { - if (ctxAccessor == null) throw new ArgumentNullException("ctxAccessor"); - _ctxAccessor = ctxAccessor; + if (httpAccessor == null) throw new ArgumentNullException("httpAccessor"); + _httpAccessor = httpAccessor; } public EventMessages Get() { - if (_ctxAccessor.Value.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name] == null) + if (_httpAccessor.Value.Items[typeof (RequestLifespanMessagesFactory).Name] == null) { - _ctxAccessor.Value.HttpContext.Items[typeof(RequestLifespanMessagesFactory).Name] = new EventMessages(); + _httpAccessor.Value.Items[typeof(RequestLifespanMessagesFactory).Name] = new EventMessages(); } - return (EventMessages)_ctxAccessor.Value.HttpContext.Items[typeof (RequestLifespanMessagesFactory).Name]; + return (EventMessages)_httpAccessor.Value.Items[typeof (RequestLifespanMessagesFactory).Name]; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/RouteDataExtensions.cs b/src/Umbraco.Web/RouteDataExtensions.cs new file mode 100644 index 0000000000..036d30c746 --- /dev/null +++ b/src/Umbraco.Web/RouteDataExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Web.Routing; + +namespace Umbraco.Web +{ + public static class RouteDataExtensions + { + /// + /// Tries to get the Umbraco context from the DataTokens + /// + /// + /// + /// + /// This is useful when working on async threads since the UmbracoContext is not copied over explicitly + /// + public static UmbracoContext GetUmbracoContext(this RouteData routeData) + { + if (routeData == null) throw new ArgumentNullException("routeData"); + + if (routeData.DataTokens.ContainsKey(Core.Constants.Web.UmbracoContextDataToken)) + { + var umbCtx = routeData.DataTokens[Core.Constants.Web.UmbracoContextDataToken] as UmbracoContext; + return umbCtx; + } + return null; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs index 22c4364891..6922510de8 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System.Globalization; +using System.Linq; using System.Web; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -20,15 +22,39 @@ namespace Umbraco.Web.Routing { LogHelper.Debug("Looking for a page to handle 404."); + // try to find a culture as best as we can + var errorCulture = CultureInfo.CurrentUICulture; + if (pcr.HasDomain) + { + errorCulture = CultureInfo.GetCultureInfo(pcr.UmbracoDomain.LanguageIsoCode); + } + else + { + var route = pcr.Uri.GetAbsolutePathDecoded(); + var pos = route.LastIndexOf('/'); + IPublishedContent node = null; + while (pos > 1) + { + route = route.Substring(0, pos); + node = pcr.RoutingContext.UmbracoContext.ContentCache.GetByRoute(route); + if (node != null) break; + pos = route.LastIndexOf('/'); + } + if (node != null) + { + var d = DomainHelper.FindWildcardDomainInPath(pcr.RoutingContext.UmbracoContext.Application.Services.DomainService.GetAll(true), node.Path, null); + if (d != null && string.IsNullOrWhiteSpace(d.LanguageIsoCode) == false) + errorCulture = CultureInfo.GetCultureInfo(d.LanguageIsoCode); + } + } + // TODO - replace the whole logic var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId( //TODO: The IContentSection should be ctor injected into this class in v8! UmbracoConfig.For.UmbracoSettings().Content.Error404Collection.ToArray(), - //TODO: Is there a better way to extract this value? at least we're not relying on singletons here though - pcr.RoutingContext.UmbracoContext.HttpContext.Request.ServerVariables["SERVER_NAME"], pcr.RoutingContext.UmbracoContext.Application.Services.EntityService, new PublishedContentQuery(pcr.RoutingContext.UmbracoContext.ContentCache, pcr.RoutingContext.UmbracoContext.MediaCache), - pcr.RoutingContext.UmbracoContext.Application.Services.DomainService); + errorCulture); IPublishedContent content = null; diff --git a/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs b/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs index 2bb2903aa0..989cabe388 100644 --- a/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs +++ b/src/Umbraco.Web/Routing/CustomRouteUrlProvider.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Routing if (umbracoContext.HttpContext.Request.RequestContext == null) return null; if (umbracoContext.HttpContext.Request.RequestContext.RouteData == null) return null; if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens == null) return null; - if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens.ContainsKey("umbraco-custom-route") == false) return null; + if (umbracoContext.HttpContext.Request.RequestContext.RouteData.DataTokens.ContainsKey(Umbraco.Core.Constants.Web.CustomRouteDataToken) == false) return null; //ok so it's a custom route with published content assigned, check if the id being requested for is the same id as the assigned published content return id == umbracoContext.PublishedContentRequest.PublishedContent.Id ? umbracoContext.PublishedContentRequest.PublishedContent.Url diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index f15c485560..915636889d 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -52,6 +52,7 @@ namespace Umbraco.Web.Routing // will not use cache if previewing var route = umbracoContext.ContentCache.GetRouteById(id); + if (string.IsNullOrWhiteSpace(route)) { LogHelper.Debug( @@ -60,6 +61,14 @@ namespace Umbraco.Web.Routing return null; } + if (route.StartsWith("err/")) + { + LogHelper.Debug( + "Page with nodeId={0} has a colliding url with page with nodeId={1}.", + () => id, () => route.Substring(4)); + return "#err-" + route.Substring(4); + } + var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); // extract domainUri and path @@ -102,6 +111,9 @@ namespace Umbraco.Web.Routing return null; } + if (route.StartsWith("err/")) + return null; + var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); // extract domainUri and path diff --git a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs index 50497ba032..94da38e210 100644 --- a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs +++ b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Web; using System.Xml; @@ -232,47 +233,29 @@ namespace Umbraco.Web.Routing /// /// internal static int? GetCurrentNotFoundPageId( - IContentErrorPage[] error404Collection, - string requestServerName, + IContentErrorPage[] error404Collection, + string requestServerName, IEntityService entityService, ITypedPublishedContentQuery publishedContentQuery, IDomainService domainService) { - if (error404Collection.Count() > 1) + throw new NotImplementedException(); + } + + internal static int? GetCurrentNotFoundPageId( + IContentErrorPage[] error404Collection, + IEntityService entityService, + ITypedPublishedContentQuery publishedContentQuery, + CultureInfo errorCulture) + { + if (error404Collection.Length > 1) { - // try to get the 404 based on current culture (via domain) - IContentErrorPage cultureErr; - - var d = domainService.GetByName(requestServerName); - - if (d != null && d.LanguageId.HasValue) - { - // test if a 404 page exists with current culture - cultureErr = error404Collection - .FirstOrDefault(x => x.Culture == d.LanguageIsoCode); - - if (cultureErr != null) - { - return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery); - } - } - // test if a 404 page exists with current culture thread - cultureErr = error404Collection - .FirstOrDefault(x => x.Culture == System.Threading.Thread.CurrentThread.CurrentUICulture.Name); - if (cultureErr != null) - { - return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery); - } - - // there should be a default one! - cultureErr = error404Collection - .FirstOrDefault(x => x.Culture == "default"); + var cultureErr = error404Collection.FirstOrDefault(x => x.Culture == errorCulture.Name) + ?? error404Collection.FirstOrDefault(x => x.Culture == "default"); // there should be a default one! if (cultureErr != null) - { return GetContentIdFromErrorPageConfig(cultureErr, entityService, publishedContentQuery); - } } else { diff --git a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs index df5584ce11..d737885020 100644 --- a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs +++ b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs @@ -44,8 +44,6 @@ namespace Umbraco.Web.Routing response.Write("

    This page can be replaced with a custom 404. Check the documentation for \"custom 404\".

    "); response.Write("

    This page is intentionally left ugly ;-)

    "); response.Write(""); - - response.End(); } public bool IsReusable diff --git a/src/Umbraco.Web/Routing/PublishedContentRequest.cs b/src/Umbraco.Web/Routing/PublishedContentRequest.cs index 78f4449c88..1ed0cf8150 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequest.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequest.cs @@ -21,6 +21,15 @@ namespace Umbraco.Web.Routing public class PublishedContentRequest { private bool _readonly; + private bool _readonlyUri; + + /// + /// Triggers before the published content request is prepared. + /// + /// When the event triggers, no preparation has been done. It is still possible to + /// modify the request's Uri property, for example to restore its original, public-facing value + /// that might have been modified by an in-between equipement such as a load-balancer. + public static event EventHandler Preparing; /// /// Triggers once the published content request has been prepared, but before it is processed. @@ -35,6 +44,10 @@ namespace Umbraco.Web.Routing // the content request is just a data holder private readonly PublishedContentRequestEngine _engine; + // the cleaned up uri + // the cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. + private Uri _uri; + /// /// Initializes a new instance of the class with a specific Uri and routing context. /// @@ -102,13 +115,23 @@ namespace Umbraco.Web.Routing _readonly = __readonly; } + /// + /// Triggers the Preparing event. + /// + internal void OnPreparing() + { + var handler = Preparing; + if (handler != null) handler(this, EventArgs.Empty); + _readonlyUri = true; + } + /// /// Triggers the Prepared event. /// internal void OnPrepared() { - if (Prepared != null) - Prepared(this, EventArgs.Empty); + var handler = Prepared; + if (handler != null) handler(this, EventArgs.Empty); if (HasPublishedContent == false) Is404 = true; // safety @@ -120,7 +143,18 @@ namespace Umbraco.Web.Routing /// Gets or sets the cleaned up Uri used for routing. /// /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. - public Uri Uri { get; private set; } + public Uri Uri { + get + { + return _uri; + } + set + { + if (_readonlyUri) + throw new InvalidOperationException("Cannot modify Uri after Preparing has triggered."); + _uri = value; + } + } private void EnsureWriteable() { @@ -166,7 +200,8 @@ namespace Umbraco.Web.Routing /// preserve or reset the template, if any. public void SetInternalRedirectPublishedContent(IPublishedContent content) { - EnsureWriteable(); + if (content == null) throw new ArgumentNullException("content"); + EnsureWriteable(); // unless a template has been set already by the finder, // template should be null at that point. diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index 65371ecba4..03a50a4b99 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -84,6 +84,11 @@ namespace Umbraco.Web.Routing // so that they point to a non-existing page eg /redirect-404.aspx // TODO: SD: We need more information on this for when we release 4.10.0 as I'm not sure what this means. + // trigger the Preparing event - at that point anything can still be changed + // the idea is that it is possible to change the uri + // + _pcr.OnPreparing(); + //find domain FindDomain(); @@ -515,11 +520,11 @@ namespace Umbraco.Web.Routing { // redirect to another page var node = _routingContext.UmbracoContext.ContentCache.GetById(internalRedirectId); - - _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + if (node != null) { - redirect = true; + _pcr.SetInternalRedirectPublishedContent(node); // don't use .PublishedContent here + redirect = true; ProfilingLogger.Logger.Debug("{0}Redirecting to id={1}", () => tracePrefix, () => internalRedirectId); } else diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index 91860e03e6..0a20f51da7 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -48,6 +48,30 @@ namespace Umbraco.Web.Routing else urls.Add(ui.Text("content", "parentNotPublished", parent.Name, umbracoContext.Security.CurrentUser)); } + else if (url.StartsWith("#err-")) + { + // route error, report + var id = int.Parse(url.Substring(5)); + var o = umbracoContext.ContentCache.GetById(id); + string s; + if (o == null) + { + s = "(unknown)"; + } + else + { + var l = new List(); + while (o != null) + { + l.Add(o.Name); + o = o.Parent; + } + l.Reverse(); + s = "/" + string.Join("/", l) + " (id=" + id + ")"; + + } + urls.Add(ui.Text("content", "routeError", s, umbracoContext.Security.CurrentUser)); + } else { urls.Add(url); diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index afa5eb8132..d63fbb4606 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -217,7 +217,11 @@ namespace Umbraco.Web.Scheduling { lock (_locker) { - if (_isCompleted) return false; + if (_isCompleted) + { + _logger.Debug(_logPrefix + "Task cannot be added {0}, the task runner is already shutdown", task.GetType); + return false; + } // add task _logger.Debug(_logPrefix + "Task added {0}", task.GetType); @@ -301,7 +305,9 @@ namespace Umbraco.Web.Scheduling // tasks in the queue will be executed... if (wait == false) return; - _runningTask.Wait(); // wait for whatever is running to end... + + if (_runningTask != null) + _runningTask.Wait(); // wait for whatever is running to end... } /// diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 0557e56e57..380ae85401 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Scheduling // ensure we do not run if not main domain, but do NOT lock it if (_appContext.MainDom.IsMainDom == false) { - LogHelper.Debug("Does not run if not MainDom."); + LogHelper.Debug("Does not run if not MainDom."); return false; // do NOT repeat, going down } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 37d7a190f7..3f0a9f2a97 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -70,7 +70,7 @@ namespace Umbraco.Web.Scheduling try { - var result = await wc.SendAsync(request, token); + var result = await wc.SendAsync(request, token).ConfigureAwait(false); // ConfigureAwait(false) is recommended? http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html return result.StatusCode == HttpStatusCode.OK; } catch (Exception ex) diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index f1f48f141a..82dd32b870 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -1,7 +1,10 @@ -using System.Web; +using System.Collections.Generic; +using System.Threading; +using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Web.Routing; namespace Umbraco.Web.Scheduling { @@ -14,60 +17,74 @@ namespace Umbraco.Web.Scheduling /// internal sealed class Scheduler : ApplicationEventHandler { - private static BackgroundTaskRunner _keepAliveRunner; - private static BackgroundTaskRunner _publishingRunner; - private static BackgroundTaskRunner _tasksRunner; - private static BackgroundTaskRunner _scrubberRunner; - private static volatile bool _started; - private static readonly object Locker = new object(); + private BackgroundTaskRunner _keepAliveRunner; + private BackgroundTaskRunner _publishingRunner; + private BackgroundTaskRunner _tasksRunner; + private BackgroundTaskRunner _scrubberRunner; + private bool _started = false; + private object _locker = new object(); + private IBackgroundTask[] _tasks; protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { if (umbracoApplication.Context == null) return; - //subscribe to app init so we can subsribe to the application events - UmbracoApplicationBase.ApplicationInit += (sender, args) => + // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly + _keepAliveRunner = new BackgroundTaskRunner("KeepAlive", applicationContext.ProfilingLogger.Logger); + _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", applicationContext.ProfilingLogger.Logger); + _tasksRunner = new BackgroundTaskRunner("ScheduledTasks", applicationContext.ProfilingLogger.Logger); + _scrubberRunner = new BackgroundTaskRunner("LogScrubber", applicationContext.ProfilingLogger.Logger); + + //We will start the whole process when a successful request is made + UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; + } + + private void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) + { + switch (e.Outcome) { - var app = (HttpApplication)sender; + case EnsureRoutableOutcome.IsRoutable: + case EnsureRoutableOutcome.NotDocumentRequest: + RegisterBackgroundTasks(e); + break; + } + } - //subscribe to the end of a successful request (a handler actually executed) - app.PostRequestHandlerExecute += (o, eventArgs) => + private void RegisterBackgroundTasks(UmbracoRequestEventArgs e) + { + //remove handler, we're done + UmbracoModule.RouteAttempt -= UmbracoModuleRouteAttempt; + + LazyInitializer.EnsureInitialized(ref _tasks, ref _started, ref _locker, () => + { + LogHelper.Debug(() => "Initializing the scheduler"); + var settings = UmbracoConfig.For.UmbracoSettings(); + + var tasks = new List { - if (_started == false) - { - lock (Locker) - { - if (_started == false) - { - _started = true; - LogHelper.Debug(() => "Initializing the scheduler"); - - // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly - _keepAliveRunner = new BackgroundTaskRunner("KeepAlive", applicationContext.ProfilingLogger.Logger); - _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", applicationContext.ProfilingLogger.Logger); - _tasksRunner = new BackgroundTaskRunner("ScheduledTasks", applicationContext.ProfilingLogger.Logger); - _scrubberRunner = new BackgroundTaskRunner("LogScrubber", applicationContext.ProfilingLogger.Logger); - - var settings = UmbracoConfig.For.UmbracoSettings(); - - // ping/keepalive - // on all servers - _keepAliveRunner.Add(new KeepAlive(_keepAliveRunner, 60000, 300000, applicationContext)); - - // scheduled publishing/unpublishing - // install on all, will only run on non-slaves servers - _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); - _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); - - // log scrubbing - // install on all, will only run on non-slaves servers - _scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings)); - } - } - } + new KeepAlive(_keepAliveRunner, 60000, 300000, e.UmbracoContext.Application), + new ScheduledPublishing(_publishingRunner, 60000, 60000, e.UmbracoContext.Application, settings), + new ScheduledTasks(_tasksRunner, 60000, 60000, e.UmbracoContext.Application, settings), + new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), e.UmbracoContext.Application, settings) }; - }; + + // ping/keepalive + // on all servers + _keepAliveRunner.TryAdd(tasks[0]); + + // scheduled publishing/unpublishing + // install on all, will only run on non-slaves servers + _publishingRunner.TryAdd(tasks[1]); + + _tasksRunner.TryAdd(tasks[2]); + + // log scrubbing + // install on all, will only run on non-slaves servers + _scrubberRunner.TryAdd(tasks[3]); + + return tasks.ToArray(); + }); } } } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 317ae7f729..be4c8923d7 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -21,17 +24,36 @@ namespace Umbraco.Web.Security.Identity { public static class AppBuilderExtensions { + /// + /// Called at the end of configuring middleware + /// + /// + /// + /// This could be used for something else in the future - maybe to inform Umbraco that middleware is done/ready, but for + /// now this is used to raise the custom event + /// + /// This is an extension method in case developer entirely replace the UmbracoDefaultOwinStartup class, in which case they will + /// need to ensure they call this extension method in their startup class. + /// + /// TODO: Move this method in v8, it doesn't belong in this namespace/extension class + /// + public static void FinalizeMiddlewareConfiguration(this IAppBuilder app) + { + UmbracoDefaultOwinStartup.OnMiddlewareConfigured(new OwinMiddlewareConfiguredEventArgs(app)); + } + /// /// Sets the OWIN logger to use Umbraco's logging system /// /// + /// + /// TODO: Move this method in v8, it doesn't belong in this namespace/extension class + /// public static void SetUmbracoLoggerFactory(this IAppBuilder app) { app.SetLoggerFactory(new OwinLoggerFactory()); } - - #region Backoffice - + /// /// Configure Default Identity User Manager for Umbraco /// @@ -45,9 +67,6 @@ namespace Umbraco.Web.Security.Identity if (appContext == null) throw new ArgumentNullException("appContext"); if (userMembershipProvider == null) throw new ArgumentNullException("userMembershipProvider"); - //Don't proceed if the app is not ready - if (appContext.IsUpgrading == false && appContext.IsConfigured == false) return; - //Configure Umbraco user manager to be created per request app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( @@ -76,9 +95,6 @@ namespace Umbraco.Web.Security.Identity if (userMembershipProvider == null) throw new ArgumentNullException("userMembershipProvider"); if (customUserStore == null) throw new ArgumentNullException("customUserStore"); - //Don't proceed if the app is not ready - if (appContext.IsUpgrading == false && appContext.IsConfigured == false) return; - //Configure Umbraco user manager to be created per request app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( @@ -105,9 +121,6 @@ namespace Umbraco.Web.Security.Identity if (appContext == null) throw new ArgumentNullException("appContext"); if (userManager == null) throw new ArgumentNullException("userManager"); - //Don't proceed if the app is not ready - if (appContext.IsUpgrading == false && appContext.IsConfigured == false) return; - //Configure Umbraco user manager to be created per request app.CreatePerOwinContext(userManager); @@ -121,43 +134,87 @@ namespace Umbraco.Web.Security.Identity /// /// /// + /// + /// By default this will be configured to execute on PipelineStage.Authenticate + /// public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + return app.UseUmbracoBackOfficeCookieAuthentication(appContext, PipelineStage.Authenticate); + } + + /// + /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline + /// + /// + /// + /// + /// Configurable pipeline stage + /// + /// + public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, ApplicationContext appContext, PipelineStage stage) { if (app == null) throw new ArgumentNullException("app"); if (appContext == null) throw new ArgumentNullException("appContext"); - //Don't proceed if the app is not ready - if (appContext.IsUpgrading == false && appContext.IsConfigured == false) return app; - - var authOptions = new UmbracoBackOfficeCookieAuthOptions( - UmbracoConfig.For.UmbracoSettings().Security, - GlobalSettings.TimeOutInMinutes, - GlobalSettings.UseSSL) + var cookieAuthProvider = new BackOfficeCookieAuthenticationProvider { - Provider = new BackOfficeCookieAuthenticationProvider - { - // Enables the application to validate the security stamp when the user - // logs in. This is a security feature which is used when you - // change a password or add an external login to your account. - OnValidateIdentity = SecurityStampValidator - .OnValidateIdentity( - TimeSpan.FromMinutes(30), - (manager, user) => user.GenerateUserIdentityAsync(manager), - identity => identity.GetUserId()), - } + // Enables the application to validate the security stamp when the user + // logs in. This is a security feature which is used when you + // change a password or add an external login to your account. + OnValidateIdentity = SecurityStampValidator + .OnValidateIdentity( + TimeSpan.FromMinutes(30), + (manager, user) => user.GenerateUserIdentityAsync(manager), + identity => identity.GetUserId()), }; - //This is a custom middleware, we need to return the user's remaining logged in seconds - app.Use( - authOptions, - UmbracoConfig.For.UmbracoSettings().Security, - app.CreateLogger()); + var authOptions = CreateCookieAuthOptions(); + authOptions.Provider = cookieAuthProvider; - app.UseCookieAuthentication(authOptions); + app.UseUmbracoBackOfficeCookieAuthentication(authOptions, appContext, stage); + + //don't apply if app isnot ready + if (appContext.IsUpgrading || appContext.IsConfigured) + { + var getSecondsOptions = CreateCookieAuthOptions( + //This defines the explicit path read cookies from for this middleware + new[] {string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path)}); + getSecondsOptions.Provider = cookieAuthProvider; + + //This is a custom middleware, we need to return the user's remaining logged in seconds + app.Use( + getSecondsOptions, + UmbracoConfig.For.UmbracoSettings().Security, + app.CreateLogger()); + } return app; } + internal static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options, ApplicationContext appContext, PipelineStage stage = PipelineStage.Authenticate) + { + if (app == null) + { + throw new ArgumentNullException("app"); + } + + //First the normal cookie middleware + app.Use(typeof(CookieAuthenticationMiddleware), app, options); + //don't apply if app isnot ready + if (appContext.IsUpgrading || appContext.IsConfigured) + { + //Then our custom middlewares + app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, new SingletonUmbracoContextAccessor()); + app.Use(typeof(FixWindowsAuthMiddlware)); + } + + //Marks all of the above middlewares to execute on Authenticate + app.UseStageMarker(stage); + + return app; + } + + /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct /// Umbraco back office configuration @@ -165,14 +222,27 @@ namespace Umbraco.Web.Security.Identity /// /// /// + /// + /// By default this will be configured to execute on PipelineStage.Authenticate + /// public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + return app.UseUmbracoBackOfficeExternalCookieAuthentication(appContext, PipelineStage.Authenticate); + } + + /// + /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct + /// Umbraco back office configuration + /// + /// + /// + /// + /// + public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, ApplicationContext appContext, PipelineStage stage) { if (app == null) throw new ArgumentNullException("app"); if (appContext == null) throw new ArgumentNullException("appContext"); - //Don't proceed if the app is not ready - if (appContext.IsUpgrading == false && appContext.IsConfigured == false) return app; - app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, @@ -185,11 +255,82 @@ namespace Umbraco.Web.Security.Identity CookieSecure = GlobalSettings.UseSSL ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest, CookieHttpOnly = true, CookieDomain = UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain - }); + }, stage); return app; } - #endregion + /// + /// In order for preview to work this needs to be called + /// + /// + /// + /// + /// + /// This ensures that during a preview request that the back office use is also Authenticated and that the back office Identity + /// is added as a secondary identity to the current IPrincipal so it can be used to Authorize the previewed document. + /// + /// + /// By default this will be configured to execute on PipelineStage.PostAuthenticate + /// + public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + return app.UseUmbracoPreviewAuthentication(appContext, PipelineStage.PostAuthenticate); + } + + /// + /// In order for preview to work this needs to be called + /// + /// + /// + /// + /// + /// + /// This ensures that during a preview request that the back office use is also Authenticated and that the back office Identity + /// is added as a secondary identity to the current IPrincipal so it can be used to Authorize the previewed document. + /// + public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, ApplicationContext appContext, PipelineStage stage) + { + //don't apply if app isnot ready + if (appContext.IsConfigured) + { + var authOptions = CreateCookieAuthOptions(); + app.Use(typeof(PreviewAuthenticationMiddleware), authOptions); + + //This middleware must execute at least on PostAuthentication, by default it is on Authorize + // The middleware needs to execute after the RoleManagerModule executes which is during PostAuthenticate, + // currently I've had 100% success with ensuring this fires after RoleManagerModule even if this is set + // to PostAuthenticate though not sure if that's always a guarantee so by default it's Authorize. + if (stage < PipelineStage.PostAuthenticate) + { + throw new InvalidOperationException("The stage specified for UseUmbracoPreviewAuthentication must be greater than or equal to " + PipelineStage.PostAuthenticate); + } + + + app.UseStageMarker(stage); + } + + return app; + } + + public static void SanitizeThreadCulture(this IAppBuilder app) + { + Thread.CurrentThread.SanitizeThreadCulture(); + } + + /// + /// Create the default umb cookie auth options + /// + /// + /// + private static UmbracoBackOfficeCookieAuthOptions CreateCookieAuthOptions(string[] explicitPaths = null) + { + var authOptions = new UmbracoBackOfficeCookieAuthOptions( + explicitPaths, + UmbracoConfig.For.UmbracoSettings().Security, + GlobalSettings.TimeOutInMinutes, + GlobalSettings.UseSSL); + return authOptions; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs index 97b7e85e37..7074a6db9f 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Owin; using Microsoft.Owin.Security; using Umbraco.Core; @@ -7,6 +8,22 @@ namespace Umbraco.Web.Security.Identity { public static class AuthenticationOptionsExtensions { + + public static void SetSignInChallengeResultCallback( + this AuthenticationOptions authOptions, + Func authProperties) + { + authOptions.Description.Properties["ChallengeResultCallback"] = authProperties; + } + + public static AuthenticationProperties GetSignInChallengeResult(this AuthenticationDescription authenticationDescription, IOwinContext ctx) + { + if (authenticationDescription.Properties.ContainsKey("ChallengeResultCallback") == false) return null; + var cb = authenticationDescription.Properties["ChallengeResultCallback"] as Func; + if (cb == null) return null; + return cb(ctx); + } + /// /// Used during the External authentication process to assign external sign-in options /// that are used by the Umbraco authentication process. diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs index 841c94c80c..07f4a3317f 100644 --- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs @@ -1,6 +1,12 @@ -using Microsoft.Owin; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Microsoft.Owin; using Microsoft.Owin.Infrastructure; using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; namespace Umbraco.Web.Security.Identity { @@ -14,10 +20,19 @@ namespace Umbraco.Web.Security.Identity internal class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager { private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly string[] _explicitPaths; + private readonly string _getRemainingSecondsPath; public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor) + : this(umbracoContextAccessor, null) + { + + } + public BackOfficeCookieManager(IUmbracoContextAccessor umbracoContextAccessor, IEnumerable explicitPaths) { _umbracoContextAccessor = umbracoContextAccessor; + _explicitPaths = explicitPaths == null ? null : explicitPaths.ToArray(); + _getRemainingSecondsPath = string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path); } /// @@ -33,8 +48,8 @@ namespace Umbraco.Web.Security.Identity return null; } - return UmbracoModule.ShouldAuthenticateRequest( - context.HttpContextFromOwinContext().Request, + return ShouldAuthenticateRequest( + context, _umbracoContextAccessor.Value.OriginalRequestUrl) == false //Don't auth request, don't return a cookie ? null @@ -42,5 +57,58 @@ namespace Umbraco.Web.Security.Identity : base.GetRequestCookie(context, key); } + /// + /// Determines if we should authenticate the request + /// + /// + /// + /// + /// + /// + /// We auth the request when: + /// * it is a back office request + /// * it is an installer request + /// * it is a /base request + /// * it is a preview request + /// + internal bool ShouldAuthenticateRequest(IOwinContext ctx, Uri originalRequestUrl, bool checkForceAuthTokens = true) + { + if (_umbracoContextAccessor.Value.Application.IsConfigured == false + && _umbracoContextAccessor.Value.Application.DatabaseContext.IsDatabaseConfigured == false) + { + //Do not authenticate the request if we don't have a db and we are not configured - since we will never need + // to know a current user in this scenario - we treat it as a new install. Without this we can have some issues + // when people have older invalid cookies on the same domain since our user managers might attempt to lookup a user + // and we don't even have a db. + return false; + } + + var request = ctx.Request; + var httpCtx = ctx.TryGetHttpContext(); + + //check the explicit paths + if (_explicitPaths != null) + { + return _explicitPaths.Any(x => x.InvariantEquals(request.Uri.AbsolutePath)); + } + + //check user seconds path + if (request.Uri.AbsolutePath.InvariantEquals(_getRemainingSecondsPath)) return false; + + if (//check the explicit flag + (checkForceAuthTokens && ctx.Get("umbraco-force-auth") != null) + || (checkForceAuthTokens && httpCtx.Success && httpCtx.Result.Items["umbraco-force-auth"] != null) + //check back office + || request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) + //check installer + || request.Uri.IsInstallerRequest() + //check for base + || BaseRest.BaseRestHandler.IsBaseRestRequest(originalRequestUrl)) + { + return true; + } + return false; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/FixWindowsAuthMiddlware.cs b/src/Umbraco.Web/Security/Identity/FixWindowsAuthMiddlware.cs new file mode 100644 index 0000000000..911a304019 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/FixWindowsAuthMiddlware.cs @@ -0,0 +1,51 @@ +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.Owin; +using Umbraco.Core; +using Umbraco.Core.Security; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// This is used to inspect the request to see if 2 x identities are assigned: A windows one and a back office one. + /// When this is the case, it means that auth has executed for Windows & auth has executed for our back office cookie + /// handler and now two identities have been assigned. Unfortunately, at some stage in the pipeline - I'm pretty sure + /// it's the Role Provider Module - it again changes the user's Principal to a RolePrincipal and discards the second + /// Identity which is the Back office identity thus preventing a user from accessing the back office... it's very annoying. + /// + /// To fix this, we re-set the user Principal to only have a single identity: the back office one, since we know this is + /// for a back office request. + /// + internal class FixWindowsAuthMiddlware : OwinMiddleware + { + public FixWindowsAuthMiddlware(OwinMiddleware next) : base(next) + { + } + + public override async Task Invoke(IOwinContext context) + { + if (context.Request.Uri.IsClientSideRequest() == false) + { + var claimsPrincipal = context.Request.User as ClaimsPrincipal; + if (claimsPrincipal != null + && claimsPrincipal.Identities.Count() > 1 + && claimsPrincipal.Identities.Any(x => x is WindowsIdentity) + && claimsPrincipal.Identities.Any(x => x is UmbracoBackOfficeIdentity)) + { + var backOfficeIdentity = claimsPrincipal.Identities.First(x => x is UmbracoBackOfficeIdentity); + if (backOfficeIdentity.IsAuthenticated) + { + context.Request.User = new ClaimsPrincipal(backOfficeIdentity); + } + } + } + + if (Next != null) + { + await Next.Invoke(context); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs new file mode 100644 index 0000000000..c7030b2558 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationHandler.cs @@ -0,0 +1,128 @@ +using System; +using Umbraco.Core; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.Infrastructure; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// If a flag is set on the context to force renew the ticket, this will do it + /// + internal class ForceRenewalCookieAuthenticationHandler : AuthenticationHandler + { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public ForceRenewalCookieAuthenticationHandler(IUmbracoContextAccessor umbracoContextAccessor) + { + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// This handler doesn't actually do any auth so we return null; + /// + /// + protected override Task AuthenticateCoreAsync() + { + return Task.FromResult((AuthenticationTicket)null); + } + + /// + /// Gets the ticket from the request + /// + /// + private AuthenticationTicket GetTicket() + { + var cookie = Options.CookieManager.GetRequestCookie(Context, Options.CookieName); + if (string.IsNullOrWhiteSpace(cookie)) + { + return null; + } + var ticket = Options.TicketDataFormat.Unprotect(cookie); + if (ticket == null) + { + return null; + } + return ticket; + } + + /// + /// This will check if the token exists in the request to force renewal + /// + /// + protected override Task ApplyResponseGrantAsync() + { + if (_umbracoContextAccessor.Value == null || Context.Request.Uri.IsClientSideRequest()) + { + return Task.FromResult(0); + } + + //Now we need to check if we should force renew this based on a flag in the context and whether this is a request that is not normally renewed by OWIN... + // which means that it is not a normal URL that is authenticated. + + var normalAuthUrl = ((BackOfficeCookieManager) Options.CookieManager) + .ShouldAuthenticateRequest(Context, _umbracoContextAccessor.Value.OriginalRequestUrl, + //Pass in false, we want to know if this is a normal auth'd page + checkForceAuthTokens: false); + //This is auth'd normally, so OWIN will naturally take care of the cookie renewal + if (normalAuthUrl) return Task.FromResult(0); + + var httpCtx = Context.TryGetHttpContext(); + //check for the special flag in either the owin or http context + var shouldRenew = Context.Get("umbraco-force-auth") != null || (httpCtx.Success && httpCtx.Result.Items["umbraco-force-auth"] != null); + + if (shouldRenew) + { + var signin = Helper.LookupSignIn(Options.AuthenticationType); + var shouldSignin = signin != null; + var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); + var shouldSignout = signout != null; + + //we don't care about the sign in/sign out scenario, only renewal + if (shouldSignin == false && shouldSignout == false) + { + //get the ticket + var ticket = GetTicket(); + if (ticket != null) + { + var currentUtc = Options.SystemClock.UtcNow; + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; + + if (expiresUtc.HasValue && issuedUtc.HasValue) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + //if it's time to renew, then do it + if (timeRemaining < timeElapsed) + { + //renew the date/times + ticket.Properties.IssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + ticket.Properties.ExpiresUtc = currentUtc.Add(timeSpan); + + //now save back all the required cookie details + var cookieValue = Options.TicketDataFormat.Protect(ticket); + + var cookieOptions = ((UmbracoBackOfficeCookieAuthOptions)Options).CreateRequestCookieOptions(Context, ticket); + + Options.CookieManager.AppendResponseCookie( + Context, + Options.CookieName, + cookieValue, + cookieOptions); + } + + + } + } + } + } + + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationMiddleware.cs new file mode 100644 index 0000000000..77a7a335f0 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/ForceRenewalCookieAuthenticationMiddleware.cs @@ -0,0 +1,29 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Microsoft.Owin.Security.Infrastructure; +using Owin; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// This middleware is used simply to force renew the auth ticket if a flag to do so is found in the request + /// + internal class ForceRenewalCookieAuthenticationMiddleware : CookieAuthenticationMiddleware + { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + public ForceRenewalCookieAuthenticationMiddleware( + OwinMiddleware next, + IAppBuilder app, + UmbracoBackOfficeCookieAuthOptions options, + IUmbracoContextAccessor umbracoContextAccessor) : base(next, app, options) + { + _umbracoContextAccessor = umbracoContextAccessor; + } + + protected override AuthenticationHandler CreateHandler() + { + return new ForceRenewalCookieAuthenticationHandler(_umbracoContextAccessor); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs index d909ff76e2..8ec49c4681 100644 --- a/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs +++ b/src/Umbraco.Web/Security/Identity/GetUserSecondsMiddleWare.cs @@ -43,14 +43,10 @@ namespace Umbraco.Web.Security.Identity { var request = context.Request; var response = context.Response; - - var rootPath = context.Request.PathBase.HasValue - ? context.Request.PathBase.Value.EnsureStartsWith("/").EnsureEndsWith("/") - : "/"; - + if (request.Uri.Scheme.InvariantStartsWith("http") && request.Uri.AbsolutePath.InvariantEquals( - string.Format("{0}{1}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", rootPath, GlobalSettings.UmbracoMvcArea))) + string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path))) { var cookie = _authOptions.CookieManager.GetRequestCookie(context, _security.AuthCookieName); if (cookie.IsNullOrWhiteSpace() == false) @@ -59,7 +55,7 @@ namespace Umbraco.Web.Security.Identity if (ticket != null) { var remainingSeconds = ticket.Properties.ExpiresUtc.HasValue - ? (ticket.Properties.ExpiresUtc.Value - DateTime.Now.ToUniversalTime()).TotalSeconds + ? (ticket.Properties.ExpiresUtc.Value - _authOptions.SystemClock.UtcNow).TotalSeconds : 0; response.ContentType = "application/json; charset=utf-8"; @@ -67,44 +63,50 @@ namespace Umbraco.Web.Security.Identity response.Headers.Add("Cache-Control", new[] { "no-cache" }); response.Headers.Add("Pragma", new[] { "no-cache" }); response.Headers.Add("Expires", new[] { "-1" }); - response.Headers.Add("Date", new[] { DateTime.Now.ToUniversalTime().ToString("R") }); + response.Headers.Add("Date", new[] { _authOptions.SystemClock.UtcNow.ToString("R") }); //Ok, so here we need to check if we want to process/renew the auth ticket for each // of these requests. If that is the case, the user will really never be logged out until they // close their browser (there will be edge cases of that, especially when debugging) if (_security.KeepUserLoggedIn) { - var utcNow = DateTime.Now.ToUniversalTime(); - ticket.Properties.IssuedUtc = utcNow; - ticket.Properties.ExpiresUtc = utcNow.AddMinutes(_authOptions.LoginTimeoutMinutes); + var currentUtc = _authOptions.SystemClock.UtcNow; + var issuedUtc = ticket.Properties.IssuedUtc; + var expiresUtc = ticket.Properties.ExpiresUtc; - var cookieValue = _authOptions.TicketDataFormat.Protect(ticket); - - var cookieOptions = new CookieOptions + if (expiresUtc.HasValue && issuedUtc.HasValue) { - Path = "/", - Domain = _authOptions.CookieDomain, - Expires = DateTime.Now.AddMinutes(_authOptions.LoginTimeoutMinutes), - HttpOnly = true, - Secure = _authOptions.CookieSecure == CookieSecureOption.Always - || (_authOptions.CookieSecure == CookieSecureOption.SameAsRequest && request.Uri.Scheme.InvariantEquals("https")), - }; + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + var timeRemaining = expiresUtc.Value.Subtract(currentUtc); - _authOptions.CookieManager.AppendResponseCookie( - context, - _authOptions.CookieName, - cookieValue, - cookieOptions); + //if it's time to renew, then do it + if (timeRemaining < timeElapsed) + { + ticket.Properties.IssuedUtc = currentUtc; + var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + ticket.Properties.ExpiresUtc = currentUtc.Add(timeSpan); - remainingSeconds = (ticket.Properties.ExpiresUtc.Value - DateTime.Now.ToUniversalTime()).TotalSeconds; + var cookieValue = _authOptions.TicketDataFormat.Protect(ticket); + + var cookieOptions = _authOptions.CreateRequestCookieOptions(context, ticket); + + _authOptions.CookieManager.AppendResponseCookie( + context, + _authOptions.CookieName, + cookieValue, + cookieOptions); + + remainingSeconds = (ticket.Properties.ExpiresUtc.Value - currentUtc).TotalSeconds; + } + } } - else if (remainingSeconds <=30) + else if (remainingSeconds <= 30) { //NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in // the timeout process. _logger.WriteCore(TraceEventType.Information, 0, - string.Format("User logged will be logged out due to timeout: {0}, IP Address: {1}", ticket.Identity.Name, request.RemoteIpAddress), + string.Format("User logged will be logged out due to timeout: {0}, IP Address: {1}", ticket.Identity.Name, request.RemoteIpAddress), null, null); } diff --git a/src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs new file mode 100644 index 0000000000..7ab071ecea --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Extensions; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Security.Identity +{ + internal class PreviewAuthenticationMiddleware : OwinMiddleware + { + private readonly UmbracoBackOfficeCookieAuthOptions _cookieOptions; + + /// + /// Instantiates the middleware with an optional pointer to the next component. + /// + /// + /// + public PreviewAuthenticationMiddleware(OwinMiddleware next, + UmbracoBackOfficeCookieAuthOptions cookieOptions) : base(next) + { + _cookieOptions = cookieOptions; + } + + /// + /// Process an individual request. + /// + /// + /// + public override async Task Invoke(IOwinContext context) + { + var request = context.Request; + if (request.Uri.IsClientSideRequest() == false) + { + var claimsPrincipal = context.Request.User as ClaimsPrincipal; + var isPreview = request.HasPreviewCookie() + && claimsPrincipal != null + && request.Uri != null + && request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false; + if (isPreview) + { + //If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. + // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication + // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. + + var cookie = request.Cookies[_cookieOptions.CookieName]; + if (cookie.IsNullOrWhiteSpace() == false) + { + var unprotected = _cookieOptions.TicketDataFormat.Unprotect(cookie); + if (unprotected != null) + { + //Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Principal, this means we'll have 2 identities assigned to the principal which we can + // use to authorize the preview and allow for a back office User. + claimsPrincipal.AddIdentity(UmbracoBackOfficeIdentity.FromClaimsIdentity(unprotected.Identity)); + } + } + } + } + + if (Next != null) + { + await Next.Invoke(context); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs index 9a55e42b51..ebe457eeb8 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthOptions.cs @@ -1,6 +1,8 @@ using System; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -19,12 +21,13 @@ namespace Umbraco.Web.Security.Identity : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL) { } - - public UmbracoBackOfficeCookieAuthOptions( - ISecuritySection securitySection, - int loginTimeoutMinutes, - bool forceSsl, - bool useLegacyFormsAuthDataFormat = true) + + public UmbracoBackOfficeCookieAuthOptions( + string[] explicitPaths, + ISecuritySection securitySection, + int loginTimeoutMinutes, + bool forceSsl, + bool useLegacyFormsAuthDataFormat = true) { LoginTimeoutMinutes = loginTimeoutMinutes; AuthenticationType = Constants.Security.BackOfficeAuthenticationType; @@ -32,19 +35,60 @@ namespace Umbraco.Web.Security.Identity if (useLegacyFormsAuthDataFormat) { //If this is not explicitly set it will fall back to the default automatically - TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + TicketDataFormat = new FormsAuthenticationSecureDataFormat(LoginTimeoutMinutes); } - + SlidingExpiration = true; - ExpireTimeSpan = TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes); + ExpireTimeSpan = TimeSpan.FromMinutes(LoginTimeoutMinutes); CookieDomain = securitySection.AuthCookieDomain; CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; + CookiePath = "/"; //Custom cookie manager so we can filter requests - CookieManager = new BackOfficeCookieManager(new SingletonUmbracoContextAccessor()); - } + CookieManager = new BackOfficeCookieManager(new SingletonUmbracoContextAccessor(), explicitPaths); + } + + public UmbracoBackOfficeCookieAuthOptions( + ISecuritySection securitySection, + int loginTimeoutMinutes, + bool forceSsl, + bool useLegacyFormsAuthDataFormat = true) + : this(null, securitySection, loginTimeoutMinutes, forceSsl, useLegacyFormsAuthDataFormat) + { + } + + /// + /// Creates the cookie options for saving the auth cookie + /// + /// + /// + /// + public CookieOptions CreateRequestCookieOptions(IOwinContext ctx, AuthenticationTicket ticket) + { + if (ctx == null) throw new ArgumentNullException("ctx"); + if (ticket == null) throw new ArgumentNullException("ticket"); + + var issuedUtc = ticket.Properties.IssuedUtc ?? SystemClock.UtcNow; + var expiresUtc = ticket.Properties.ExpiresUtc ?? issuedUtc.Add(ExpireTimeSpan); + + var cookieOptions = new CookieOptions + { + Path = "/", + Domain = this.CookieDomain ?? null, + HttpOnly = true, + Secure = this.CookieSecure == CookieSecureOption.Always + || (this.CookieSecure == CookieSecureOption.SameAsRequest && ctx.Request.IsSecure), + }; + + if (ticket.Properties.IsPersistent) + { + cookieOptions.Expires = expiresUtc.UtcDateTime; + } + + return cookieOptions; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index 3bb916ed13..4f5c14723b 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Security; using Umbraco.Web.Models; using Umbraco.Web.PublishedCache; using Umbraco.Core.Cache; +using Umbraco.Web.Security.Providers; using MPE = global::Umbraco.Core.Security.MembershipProviderExtensions; namespace Umbraco.Web.Security @@ -30,7 +31,7 @@ namespace Umbraco.Web.Security #region Constructors public MembershipHelper(ApplicationContext applicationContext, HttpContextBase httpContext) - : this(applicationContext, httpContext, MPE.GetMembersMembershipProvider(), Roles.Provider) + : this(applicationContext, httpContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider : new MembersRoleProvider(applicationContext.Services.MemberService)) { } @@ -47,7 +48,7 @@ namespace Umbraco.Web.Security } public MembershipHelper(UmbracoContext umbracoContext) - : this(umbracoContext, MPE.GetMembersMembershipProvider(), Roles.Provider) + : this(umbracoContext, MPE.GetMembersMembershipProvider(), Roles.Enabled ? Roles.Provider: new MembersRoleProvider(umbracoContext.Application.Services.MemberService)) { } diff --git a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs b/src/Umbraco.Web/Security/OwinExtensions.cs similarity index 51% rename from src/Umbraco.Web/Security/Identity/OwinExtensions.cs rename to src/Umbraco.Web/Security/OwinExtensions.cs index ceb0bdafb2..d1f1fbc1ed 100644 --- a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs +++ b/src/Umbraco.Web/Security/OwinExtensions.cs @@ -1,7 +1,8 @@ -using System.Web; +using System.Web; using Microsoft.Owin; +using Umbraco.Core; -namespace Umbraco.Web.Security.Identity +namespace Umbraco.Web.Security { internal static class OwinExtensions { @@ -11,9 +12,10 @@ namespace Umbraco.Web.Security.Identity /// /// /// - internal static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) + internal static Attempt TryGetHttpContext(this IOwinContext owinContext) { - return owinContext.Get(typeof(HttpContextBase).FullName); + var ctx = owinContext.Get(typeof(HttpContextBase).FullName); + return ctx == null ? Attempt.Fail() : Attempt.Succeed(ctx); } } diff --git a/src/Umbraco.Web/Security/WebAuthExtensions.cs b/src/Umbraco.Web/Security/WebAuthExtensions.cs new file mode 100644 index 0000000000..52963d1852 --- /dev/null +++ b/src/Umbraco.Web/Security/WebAuthExtensions.cs @@ -0,0 +1,74 @@ +using System.Net.Http; +using System.Security.Claims; +using System.Security.Principal; +using System.ServiceModel.Channels; +using System.Threading; +using System.Web; +using AutoMapper; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Security; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web.Security +{ + internal static class WebAuthExtensions + { + /// + /// This will set a an authenticated IPrincipal to the current request given the IUser object + /// + /// + /// + /// + internal static IPrincipal SetPrincipalForRequest(this HttpRequestMessage request, IUser user) + { + var principal = new ClaimsPrincipal( + new UmbracoBackOfficeIdentity( + new ClaimsIdentity(), + Mapper.Map(user))); + + //It is actually not good enough to set this on the current app Context and the thread, it also needs + // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually + // an underlying fault of asp.net not propogating the User correctly. + if (HttpContext.Current != null) + { + HttpContext.Current.User = principal; + } + var http = request.TryGetHttpContext(); + if (http) + { + http.Result.User = principal; + } + Thread.CurrentPrincipal = principal; + + //For WebAPI + request.SetUserPrincipal(principal); + + return principal; + } + + /// + /// This will set a an authenticated IPrincipal to the current request given the IUser object + /// + /// + /// + /// + internal static IPrincipal SetPrincipalForRequest(this HttpContextBase httpContext, UserData userData) + { + var principal = new ClaimsPrincipal( + new UmbracoBackOfficeIdentity( + new ClaimsIdentity(), + userData)); + + //It is actually not good enough to set this on the current app Context and the thread, it also needs + // to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually + // an underlying fault of asp.net not propogating the User correctly. + if (HttpContext.Current != null) + { + HttpContext.Current.User = principal; + } + httpContext.User = principal; + Thread.CurrentPrincipal = principal; + return principal; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index d3d6f76999..eb0a1eb66a 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Web; using System.Web.Security; @@ -8,8 +9,10 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; -using umbraco; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; using umbraco.businesslogic.Exceptions; +using Umbraco.Web.Models.ContentEditing; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using User = umbraco.BusinessLogic.User; @@ -77,6 +80,42 @@ namespace Umbraco.Web.Security } } + private BackOfficeSignInManager _signInManager; + private BackOfficeSignInManager SignInManager + { + get + { + if (_signInManager == null) + { + var mgr = _httpContext.GetOwinContext().Get(); + if (mgr == null) + { + throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeSignInManager) + " from the " + typeof(IOwinContext)); + } + _signInManager = mgr; + } + return _signInManager; + } + } + + private BackOfficeUserManager _userManager; + protected BackOfficeUserManager UserManager + { + get + { + if (_userManager == null) + { + var mgr = _httpContext.GetOwinContext().GetUserManager(); + if (mgr == null) + { + throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeUserManager) + " from the " + typeof(IOwinContext) + " GetUserManager method"); + } + _userManager = mgr; + } + return _userManager; + } + } + /// /// Logs a user in. /// @@ -84,19 +123,23 @@ namespace Umbraco.Web.Security /// returns the number of seconds until their session times out public virtual double PerformLogin(int userId) { - var user = _applicationContext.Services.UserService.GetUserById(userId); - return PerformLogin(user).GetRemainingAuthSeconds(); + var owinCtx = _httpContext.GetOwinContext(); + //ensure it's done for owin too + owinCtx.Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); + + var user = UserManager.FindByIdAsync(userId).Result; + var userData = Mapper.Map(user); + _httpContext.SetPrincipalForRequest(userData); + + SignInManager.SignInAsync(user, isPersistent: true, rememberBrowser: false).Wait(); + return TimeSpan.FromMinutes(GlobalSettings.TimeOutInMinutes).TotalSeconds; } - /// - /// Logs the user in - /// - /// - /// returns the Forms Auth ticket created which is used to log them in + [Obsolete("This method should not be used, login is performed by the OWIN pipeline, use the overload that returns double and accepts a UserId instead")] public virtual FormsAuthenticationTicket PerformLogin(IUser user) { - //clear the external cookie - we do this without owin context because we're writing cookies directly to httpcontext - // and cookie handling is different with httpcontext vs webapi and owin, normally we'd do: + //clear the external cookie - we do this first without owin context because we're writing cookies directly to httpcontext + // and cookie handling is different with httpcontext vs webapi and owin, normally we'd just do: //_httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalCookieName); @@ -106,10 +149,10 @@ namespace Umbraco.Web.Security _httpContext.Response.Cookies.Set(externalLoginCookie); } - var ticket = _httpContext.CreateUmbracoAuthTicket(Mapper.Map(user)); - - LogHelper.Info("User Id: {0} logged in", () => user.Id); + //ensure it's done for owin too + _httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); + var ticket = _httpContext.CreateUmbracoAuthTicket(Mapper.Map(user)); return ticket; } @@ -119,6 +162,9 @@ namespace Umbraco.Web.Security public virtual void ClearCurrentLogin() { _httpContext.UmbracoLogout(); + _httpContext.GetOwinContext().Authentication.SignOut( + Core.Constants.Security.BackOfficeAuthenticationType, + Core.Constants.Security.BackOfficeExternalAuthenticationType); } /// @@ -416,7 +462,6 @@ namespace Umbraco.Web.Security protected override void DisposeResources() { _httpContext = null; - _applicationContext = null; } diff --git a/src/Umbraco.Web/SingletonHttpContextAccessor.cs b/src/Umbraco.Web/SingletonHttpContextAccessor.cs new file mode 100644 index 0000000000..cdeafa48e1 --- /dev/null +++ b/src/Umbraco.Web/SingletonHttpContextAccessor.cs @@ -0,0 +1,12 @@ +using System.Web; + +namespace Umbraco.Web +{ + internal class SingletonHttpContextAccessor : IHttpContextAccessor + { + public HttpContextBase Value + { + get { return new HttpContextWrapper(HttpContext.Current); } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs index 0b31f53c74..4a316fe96f 100644 --- a/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/ClearCsrfCookiesAfterUpgrade.cs @@ -1,8 +1,10 @@ -using System.Web; +using System.Linq; +using System.Web; using Umbraco.Core.Events; using Umbraco.Core.Persistence.Migrations; using Umbraco.Web.WebApi.Filters; using umbraco.interfaces; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Strategies.Migrations { @@ -13,6 +15,8 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + if (HttpContext.Current == null) return; var http = new HttpContextWrapper(HttpContext.Current); diff --git a/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs index 54b4144fc0..cf9ddeafb2 100644 --- a/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/ClearMediaXmlCacheForDeletedItemsAfterUpgrade.cs @@ -5,21 +5,24 @@ using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.SqlSyntax; using umbraco.interfaces; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Strategies.Migrations { /// /// This will execute after upgrading to remove any xml cache for media that are currently in the bin /// - /// - /// This will execute for specific versions - - /// + /// + /// This will execute for specific versions - + /// /// * If current is less than or equal to 7.0.0 /// public class ClearMediaXmlCacheForDeletedItemsAfterUpgrade : MigrationStartupHander - { + { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + var target70 = new Version(7, 0, 0); if (e.ConfiguredVersion <= target70) @@ -28,7 +31,7 @@ namespace Umbraco.Web.Strategies.Migrations // http://issues.umbraco.org/issue/U4-3876 var sql = @"DELETE FROM cmsContentXml WHERE nodeId IN - (SELECT nodeId FROM (SELECT DISTINCT cmsContentXml.nodeId FROM cmsContentXml + (SELECT nodeId FROM (SELECT DISTINCT cmsContentXml.nodeId FROM cmsContentXml INNER JOIN umbracoNode ON cmsContentXml.nodeId = umbracoNode.id WHERE nodeObjectType = '" + Constants.ObjectTypes.Media + "' AND " + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("path") + " LIKE '%-21%') x)"; diff --git a/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs b/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs index ffdd64dd57..270fe4eace 100644 --- a/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs +++ b/src/Umbraco.Web/Strategies/Migrations/EnsureListViewDataTypeIsCreated.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; using umbraco.interfaces; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Strategies.Migrations { @@ -20,6 +21,8 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + var target720 = new Version(7, 2, 0); if (e.ConfiguredVersion <= target720) @@ -95,7 +98,7 @@ namespace Umbraco.Web.Strategies.Migrations e.MigrationContext.Database.Execute(new Sql(string.Format("SET IDENTITY_INSERT {0} OFF;", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName("cmsDataTypePreValues")))); } - + transaction.Complete(); } diff --git a/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs b/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs index 6b2c92f79f..a350911580 100644 --- a/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs +++ b/src/Umbraco.Web/Strategies/Migrations/MigrationStartupHander.cs @@ -1,4 +1,5 @@ -using Umbraco.Core; +using System.Linq; +using Umbraco.Core; using Umbraco.Core.Persistence.Migrations; namespace Umbraco.Web.Strategies.Migrations @@ -41,9 +42,20 @@ namespace Umbraco.Web.Strategies.Migrations private void MigrationRunner_Migrated(MigrationRunner sender, Core.Events.MigrationEventArgs e) { - AfterMigration(sender, e); + if (TargetProductNames.Length == 0 || TargetProductNames.Contains(e.ProductName)) + { + AfterMigration(sender, e); + } } protected abstract void AfterMigration(MigrationRunner sender, Core.Events.MigrationEventArgs e); + + /// + /// Override to specify explicit target product names + /// + /// + /// Leaving empty will run for all migration products + /// + public virtual string[] TargetProductNames { get { return new string[] {}; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs b/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs index a8ef08cce0..9a8b6a6044 100644 --- a/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs +++ b/src/Umbraco.Web/Strategies/Migrations/OverwriteStylesheetFilesFromTempFiles.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Umbraco.Core.Events; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence.Migrations; @@ -22,6 +23,8 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + var target73 = new Version(7, 3, 0); if (e.ConfiguredVersion <= target73) diff --git a/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs b/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs index 3e0df97e88..7be8d818d3 100644 --- a/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs +++ b/src/Umbraco.Web/Strategies/Migrations/PublishAfterUpgradeToVersionSixth.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Persistence.UnitOfWork; using umbraco.interfaces; using Umbraco.Core; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Strategies.Migrations { @@ -19,6 +20,8 @@ namespace Umbraco.Web.Strategies.Migrations { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + var target = new Version(6, 0, 0); if (e.ConfiguredVersion < target) { @@ -69,7 +72,7 @@ namespace Umbraco.Web.Strategies.Migrations transaction.Complete(); } } - } - + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs index fad63c66dc..c3920677c5 100644 --- a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs +++ b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs @@ -4,6 +4,7 @@ using Umbraco.Core.Events; using Umbraco.Core.Persistence.Migrations; using Umbraco.Core.Services; using umbraco.interfaces; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Strategies.Migrations { @@ -12,15 +13,17 @@ namespace Umbraco.Web.Strategies.Migrations /// /// /// This cannot execute as part of a db migration since we need access to the services/repos. - /// - /// This will execute for specific versions - - /// + /// + /// This will execute for specific versions - + /// /// * If current is less than or equal to 7.0.0 /// public class RebuildMediaXmlCacheAfterUpgrade : MigrationStartupHander { protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + var target70 = new Version(7, 0, 0); if (e.ConfiguredVersion <= target70) diff --git a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs index 2a61d4177d..a6dfa07be1 100644 --- a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs +++ b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using System.Web; using Newtonsoft.Json; using Umbraco.Core; @@ -6,6 +8,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Web.Routing; +using Umbraco.Web.Scheduling; namespace Umbraco.Web.Strategies { @@ -22,74 +25,133 @@ namespace Umbraco.Web.Strategies /// public sealed class ServerRegistrationEventHandler : ApplicationEventHandler { - private readonly object _locko = new object(); private DatabaseServerRegistrar _registrar; - private DateTime _lastUpdated = DateTime.MinValue; + private BackgroundTaskRunner _backgroundTaskRunner; + private bool _started = false; + private TouchServerTask _task; + private object _lock = new object(); // bind to events protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { _registrar = ServerRegistrarResolver.Current.Registrar as DatabaseServerRegistrar; - + // only for the DatabaseServerRegistrar if (_registrar == null) return; + _backgroundTaskRunner = new BackgroundTaskRunner( + new BackgroundTaskRunnerOptions { AutoStart = true }, + applicationContext.ProfilingLogger.Logger); + + //We will start the whole process when a successful request is made UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; } - // handles route attempts. + /// + /// Handle when a request is made + /// + /// + /// + /// + /// We require this because: + /// - ApplicationContext.UmbracoApplicationUrl is initialized by UmbracoModule in BeginRequest + /// - RegisterServer is called on UmbracoModule.RouteAttempt which is triggered in ProcessRequest + /// we are safe, UmbracoApplicationUrl has been initialized + /// private void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) { - if (e.HttpContext.Request == null || e.HttpContext.Request.Url == null) return; - switch (e.Outcome) { case EnsureRoutableOutcome.IsRoutable: - // front-end request - RegisterServer(e); - break; case EnsureRoutableOutcome.NotDocumentRequest: - // anything else (back-end request, service...) - //so it's not a document request, we'll check if it's a back office request - if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) - RegisterServer(e); - break; - /* - case EnsureRoutableOutcome.NotReady: - case EnsureRoutableOutcome.NotConfigured: - case EnsureRoutableOutcome.NoContent: - default: - // otherwise, do nothing - break; - */ + RegisterBackgroundTasks(e); + break; } } - - // register current server (throttled). - private void RegisterServer(UmbracoRequestEventArgs e) + + private void RegisterBackgroundTasks(UmbracoRequestEventArgs e) { - lock (_locko) // ensure we trigger only once + //remove handler, we're done + UmbracoModule.RouteAttempt -= UmbracoModuleRouteAttempt; + + //only perform this one time ever + LazyInitializer.EnsureInitialized(ref _task, ref _started, ref _lock, () => { - var secondsSinceLastUpdate = DateTime.Now.Subtract(_lastUpdated).TotalSeconds; - if (secondsSinceLastUpdate < _registrar.Options.ThrottleSeconds) return; - _lastUpdated = DateTime.Now; + var serverAddress = e.UmbracoContext.Application.UmbracoApplicationUrl; + var svc = e.UmbracoContext.Application.Services.ServerRegistrationService; + + var task = new TouchServerTask(_backgroundTaskRunner, + 15000, //delay before first execution + _registrar.Options.RecurringSeconds*1000, //amount of ms between executions + svc, _registrar, serverAddress); + + //Perform the rest async, we don't want to block the startup sequence + // this will just reoccur on a background thread + _backgroundTaskRunner.TryAdd(task); + + return task; + }); + } + + private class TouchServerTask : RecurringTaskBase + { + private readonly IServerRegistrationService _svc; + private readonly DatabaseServerRegistrar _registrar; + private readonly string _serverAddress; + + /// + /// Initializes a new instance of the class. + /// + /// The task runner. + /// The delay. + /// The period. + /// + /// + /// + /// The task will repeat itself periodically. Use this constructor to create a new task. + public TouchServerTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + IServerRegistrationService svc, DatabaseServerRegistrar registrar, string serverAddress) + : base(runner, delayMilliseconds, periodMilliseconds) + { + if (svc == null) throw new ArgumentNullException("svc"); + _svc = svc; + _registrar = registrar; + _serverAddress = serverAddress; } - var svc = e.UmbracoContext.Application.Services.ServerRegistrationService; - - // because - // - ApplicationContext.UmbracoApplicationUrl is initialized by UmbracoModule in BeginRequest - // - RegisterServer is called on UmbracoModule.RouteAttempt which is triggered in ProcessRequest - // we are safe, UmbracoApplicationUrl has been initialized - var serverAddress = e.UmbracoContext.Application.UmbracoApplicationUrl; - - try + public override bool IsAsync { - svc.TouchServer(serverAddress, svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); + get { return false; } } - catch (Exception ex) + + public override bool RunsOnShutdown { - LogHelper.Error("Failed to update server record in database.", ex); + get { return false; } + } + + /// + /// Runs the background task. + /// + /// A value indicating whether to repeat the task. + public override bool PerformRun() + { + try + { + _svc.TouchServer(_serverAddress, _svc.CurrentServerIdentity, _registrar.Options.StaleServerTimeout); + + return true; // repeat + } + catch (Exception ex) + { + LogHelper.Error("Failed to update server record in database.", ex); + + return false; // probably stop if we have an error + } + } + + public override Task PerformRunAsync(CancellationToken token) + { + throw new NotImplementedException(); } } } diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 50267f1d05..9f33a44ea9 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Trees Constants.Applications.Developer, Constants.Applications.Members)] [LegacyBaseTree(typeof(loadContent))] - [Tree(Constants.Applications.Content, Constants.Trees.Content, "Content")] + [Tree(Constants.Applications.Content, Constants.Trees.Content)] [PluginController("UmbracoTrees")] [CoreTree] public class ContentTreeController : ContentTreeControllerBase diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs new file mode 100644 index 0000000000..1cda4995fa --- /dev/null +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using System.Net.Http.Formatting; +using umbraco; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)] + [Tree(Constants.Applications.Settings, Constants.Trees.DocumentTypes, null, sortOrder: 6)] + [Mvc.PluginController("UmbracoTrees")] + [CoreTree] + [LegacyBaseTree(typeof(loadNodeTypes))] + public class ContentTypeTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var intId = id.TryConvertTo(); + if (intId == false) throw new InvalidOperationException("Id must be an integer"); + + var nodes = new TreeNodeCollection(); + + nodes.AddRange( + Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentTypeContainer) + .OrderBy(entity => entity.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-folder", dt.HasChildren(), ""); + node.Path = dt.Path; + node.NodeType = "container"; + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + node.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + return node; + })); + + //if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + + nodes.AddRange( + Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentType) + .OrderBy(entity => entity.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", + //NOTE: This is legacy now but we need to support upgrades. From 7.4+ we don't allow 'child' creations since + // this is an organiational thing and we do that with folders now. + dt.HasChildren()); + + node.Path = dt.Path; + return node; + })); + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + var enableInheritedDocumentTypes = UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; + + if (id == Constants.System.Root.ToInvariantString()) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionImport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity + { + Id = int.Parse(id), + Level = 1, + ParentId = Constants.System.Root, + Name = "" + }, "documenttypes", "settings"); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + return menu; + } + + var container = Services.EntityService.Get(int.Parse(id), UmbracoObjectTypes.DocumentTypeContainer); + if (container != null) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + if (container.HasChildren() == false) + { + //can delete doc type + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + } + else + { + var ct = Services.ContentTypeService.GetContentType(int.Parse(id)); + IContentType parent = null; + parent = ct == null ? null : Services.ContentTypeService.GetContentType(ct.ParentId); + + if (enableInheritedDocumentTypes) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + //no move action if this is a child doc type + if (parent == null) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + } + else + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); + //no move action if this is a child doc type + if (parent == null) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionCopy.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionExport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity + { + Id = int.Parse(id), + Level = 1, + ParentId = Constants.System.Root, + Name = "" + }, "documenttypes", "settings"); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + if (enableInheritedDocumentTypes) + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); + } + + return menu; + } + } +} diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index fdc8b4c949..67da0cf355 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -12,32 +12,60 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Services; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.DataTypes)] - [Tree(Constants.Applications.Developer, Constants.Trees.DataTypes, "Data Types")] + [Tree(Constants.Applications.Developer, Constants.Trees.DataTypes)] [PluginController("UmbracoTrees")] [CoreTree] public class DataTypeTreeController : TreeController { protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { - //we only support one tree level for data types - if (id != Constants.System.Root.ToInvariantString()) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + var intId = id.TryConvertTo(); + if (intId == false) throw new InvalidOperationException("Id must be an integer"); + + var nodes = new TreeNodeCollection(); + //Folders first + nodes.AddRange( + Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataTypeContainer) + .OrderBy(entity => entity.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-folder", dt.HasChildren(), ""); + node.Path = dt.Path; + node.NodeType = "container"; + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + node.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + return node; + })); + + //if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + + //Normal nodes var sysIds = GetSystemIds(); - var collection = new TreeNodeCollection(); - collection.AddRange( - Services.DataTypeService.GetAllDataTypeDefinitions() - .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(id, queryStrings, sysIds, dt))); - return collection; + nodes.AddRange( + Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DataType) + .OrderBy(entity => entity.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Id.ToInvariantString(), id, queryStrings, dt.Name, "icon-autofill", false); + node.Path = dt.Path; + if (sysIds.Contains(dt.Id)) + { + node.Icon = "icon-thumbnail-list"; + } + return node; + })); + + return nodes; } private IEnumerable GetSystemIds() @@ -50,43 +78,47 @@ namespace Umbraco.Web.Trees }; return systemIds; } - - private TreeNode CreateTreeNode(string id, FormDataCollection queryStrings, IEnumerable systemIds, IDataTypeDefinition dt) - { - var node = CreateTreeNode( - dt.Id.ToInvariantString(), - id, - queryStrings, - dt.Name, - "icon-autofill", - false); - - if (systemIds.Contains(dt.Id)) - { - node.Icon = "icon-thumbnail-list"; - } - - return node; - } - + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { var menu = new MenuItemCollection(); if (id == Constants.System.Root.ToInvariantString()) { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + // root actions - menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); return menu; } - var sysIds = GetSystemIds(); - - if (sysIds.Contains(int.Parse(id)) == false) + var container = Services.EntityService.Get(int.Parse(id), UmbracoObjectTypes.DataTypeContainer); + if (container != null) { - //only have delete for each node - menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + if (container.HasChildren() == false) + { + //can delete data type + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); + } + else + { + var sysIds = GetSystemIds(); + + if (sysIds.Contains(int.Parse(id)) == false) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + } + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); } return menu; diff --git a/src/Umbraco.Web/Trees/LanguageTreeController.cs b/src/Umbraco.Web/Trees/LanguageTreeController.cs index fefbf82307..5481b7d40c 100644 --- a/src/Umbraco.Web/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web/Trees/LanguageTreeController.cs @@ -17,7 +17,7 @@ namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Languages)] [LegacyBaseTree(typeof(loadLanguages))] - [Tree(Constants.Applications.Settings, Constants.Trees.Languages, "Languages", sortOrder: 4)] + [Tree(Constants.Applications.Settings, Constants.Trees.Languages, null, sortOrder: 4)] [PluginController("UmbracoTrees")] [CoreTree] public class LanguageTreeController : TreeController @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees language.Id.ToString(CultureInfo.InvariantCulture), "-1", queryStrings, language.CultureInfo.DisplayName, "icon-flag-alt", false, //TODO: Rebuild the language editor in angular, then we dont need to have this at all (which is just a path to the legacy editor) "/" + queryStrings.GetValue("application") + "/framed/" + - Uri.EscapeDataString("/umbraco/settings/editLanguage.aspx?id=" + language.Id))); + Uri.EscapeDataString("settings/editLanguage.aspx?id=" + language.Id))); } } @@ -96,4 +96,4 @@ namespace Umbraco.Web.Trees return menu; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 86d7bc44c9..b08db4cacd 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Trees Constants.Applications.Developer, Constants.Applications.Members)] [LegacyBaseTree(typeof(loadMedia))] - [Tree(Constants.Applications.Media, Constants.Trees.Media, "Media")] + [Tree(Constants.Applications.Media, Constants.Trees.Media)] [PluginController("UmbracoTrees")] [CoreTree] public class MediaTreeController : ContentTreeControllerBase diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs new file mode 100644 index 0000000000..717d53ab96 --- /dev/null +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Formatting; +using System.Text; +using System.Threading.Tasks; +using umbraco; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] + [Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, null, sortOrder:5)] + [Mvc.PluginController("UmbracoTrees")] + [CoreTree] + [LegacyBaseTree(typeof(loadMediaTypes))] + public class MediaTypeTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var intId = id.TryConvertTo(); + if (intId == false) throw new InvalidOperationException("Id must be an integer"); + + var nodes = new TreeNodeCollection(); + + nodes.AddRange( + Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.MediaTypeContainer) + .OrderBy(entity => entity.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-folder", dt.HasChildren(), ""); + node.Path = dt.Path; + node.NodeType = "container"; + //TODO: This isn't the best way to ensure a noop process for clicking a node but it works for now. + node.AdditionalData["jsClickCallback"] = "javascript:void(0);"; + return node; + })); + + //if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + + nodes.AddRange( + Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.MediaType) + .OrderBy(entity => entity.Name) + .Select(dt => + { + var node = CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", false); + node.Path = dt.Path; + return node; + })); + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + // root actions + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias))); + return menu; + } + + var container = Services.EntityService.Get(int.Parse(id), UmbracoObjectTypes.MediaTypeContainer); + if (container != null) + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; + + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + + if (container.HasChildren() == false) + { + //can delete doc type + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); + } + else + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionCopy.Instance.Alias))); + } + + return menu; + } + } +} diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index e80cabecfa..fa6eb64571 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Trees Constants.Applications.Media, Constants.Applications.Members)] [LegacyBaseTree(typeof (loadMembers))] - [Tree(Constants.Applications.Members, Constants.Trees.Members, "Members")] + [Tree(Constants.Applications.Members, Constants.Trees.Members, null, sortOrder: 0)] [PluginController("UmbracoTrees")] [CoreTree] public class MemberTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs new file mode 100644 index 0000000000..d739c2a661 --- /dev/null +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Formatting; +using System.Text; +using System.Threading.Tasks; +using umbraco; +using umbraco.BusinessLogic.Actions; +using Umbraco.Core; +using Umbraco.Web.Models.Trees; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Trees +{ + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] + [Tree(Constants.Applications.Members, Constants.Trees.MemberTypes, null, sortOrder:2 )] + [Mvc.PluginController("UmbracoTrees")] + [CoreTree] + [LegacyBaseTree(typeof(loadMemberTypes))] + public class MemberTypeTreeController : TreeController + { + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) + { + var nodes = new TreeNodeCollection(); + nodes.AddRange( + Services.MemberTypeService.GetAll() + .OrderBy(x => x.Name) + .Select(dt => CreateTreeNode(dt.Id.ToString(), id, queryStrings, dt.Name, "icon-item-arrangement", false))); + + return nodes; + } + + protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) + { + var menu = new MenuItemCollection(); + + if (id == Constants.System.Root.ToInvariantString()) + { + // root actions + menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); + menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); + return menu; + } + else + { + //delete member type + menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); + } + + return menu; + } + } +} diff --git a/src/Umbraco.Web/Trees/PartialViewsTree.cs b/src/Umbraco.Web/Trees/PartialViewsTree.cs index ac937864ab..11d6e3e804 100644 --- a/src/Umbraco.Web/Trees/PartialViewsTree.cs +++ b/src/Umbraco.Web/Trees/PartialViewsTree.cs @@ -77,8 +77,8 @@ namespace Umbraco.Web.Trees protected override void OnRenderFileNode(ref XmlTreeNode xNode) { ChangeNodeAction(xNode); - xNode.Icon = "settingView.gif"; - xNode.OpenIcon = "settingView.gif"; + xNode.Icon = "icon-article"; + xNode.OpenIcon = "icon-article"; xNode.Text = xNode.Text.StripFileExtension(); } diff --git a/src/Umbraco.Web/Trees/TemplatesTreeController.cs b/src/Umbraco.Web/Trees/TemplatesTreeController.cs index 9e4b6f71d8..61cff8f862 100644 --- a/src/Umbraco.Web/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web/Trees/TemplatesTreeController.cs @@ -131,9 +131,9 @@ namespace Umbraco.Web.Trees return Services.FileService.DetermineTemplateRenderingEngine(template) == RenderingEngine.WebForms ? "/" + queryStrings.GetValue("application") + "/framed/" + - Uri.EscapeDataString("/umbraco/settings/editTemplate.aspx?templateID=" + template.Id) + Uri.EscapeDataString("settings/editTemplate.aspx?templateID=" + template.Id) : "/" + queryStrings.GetValue("application") + "/framed/" + - Uri.EscapeDataString("/umbraco/settings/Views/EditView.aspx?treeType=" + Constants.Trees.Templates + "&templateID=" + template.Id); + Uri.EscapeDataString("settings/Views/EditView.aspx?treeType=" + Constants.Trees.Templates + "&templateID=" + template.Id); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Trees/TreeAttribute.cs b/src/Umbraco.Web/Trees/TreeAttribute.cs index 72ae4044e9..e3e302fb69 100644 --- a/src/Umbraco.Web/Trees/TreeAttribute.cs +++ b/src/Umbraco.Web/Trees/TreeAttribute.cs @@ -8,6 +8,16 @@ namespace Umbraco.Web.Trees [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class TreeAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The app alias. + /// The alias. + public TreeAttribute(string appAlias, + string alias) : this(appAlias, alias, null) + { + } + /// /// Initializes a new instance of the class. /// @@ -35,6 +45,8 @@ namespace Umbraco.Web.Trees SortOrder = sortOrder; } + + public string ApplicationAlias { get; private set; } public string Alias { get; private set; } public string Title { get; private set; } diff --git a/src/Umbraco.Web/Trees/TreeController.cs b/src/Umbraco.Web/Trees/TreeController.cs index 14a25b6f02..0f975a9860 100644 --- a/src/Umbraco.Web/Trees/TreeController.cs +++ b/src/Umbraco.Web/Trees/TreeController.cs @@ -1,10 +1,15 @@ using System; using System.Collections.Concurrent; +using System.Globalization; using System.Linq; using System.Net.Http.Formatting; +using System.Threading; +using System.Web.Security; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Core.Services; namespace Umbraco.Web.Trees { @@ -13,9 +18,55 @@ namespace Umbraco.Web.Trees /// public abstract class TreeController : TreeControllerBase { - private readonly TreeAttribute _attribute; + private TreeAttribute _attribute; protected TreeController() + { + Initialize(); + } + + protected TreeController(UmbracoContext umbracoContext) : base(umbracoContext) + { + Initialize(); + } + + protected TreeController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + Initialize(); + } + + /// + /// The name to display on the root node + /// + public override string RootNodeDisplayName + { + get + { + + //if title is defined, return that + if(string.IsNullOrEmpty(_attribute.Title) == false) + return _attribute.Title; + + + //try to look up a tree header matching the tree alias + var localizedLabel = Services.TextService.Localize("treeHeaders/" + _attribute.Alias); + if (string.IsNullOrEmpty(localizedLabel) == false) + return localizedLabel; + + //is returned to signal that a label was not found + return "[" + _attribute.Alias + "]"; + } + } + + /// + /// Gets the current tree alias from the attribute assigned to it. + /// + public override string TreeAlias + { + get { return _attribute.Alias; } + } + + private void Initialize() { //Locate the tree attribute var treeAttributes = GetType() @@ -31,22 +82,5 @@ namespace Umbraco.Web.Trees //assign the properties of this object to those of the metadata attribute _attribute = treeAttributes.First(); } - - /// - /// The name to display on the root node - /// - public override string RootNodeDisplayName - { - get { return _attribute.Title; } - } - - /// - /// Gets the current tree alias from the attribute assigned to it. - /// - public override string TreeAlias - { - get { return _attribute.Alias; } - } - } } diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 74b2ffc670..91191a165d 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -18,6 +18,18 @@ namespace Umbraco.Web.Trees [AngularJsonOnlyConfiguration] public abstract class TreeControllerBase : UmbracoAuthorizedApiController { + protected TreeControllerBase() + { + } + + protected TreeControllerBase(UmbracoContext umbracoContext) : base(umbracoContext) + { + } + + protected TreeControllerBase(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } + /// /// The method called to render the contents of the tree structure /// diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index cec31154d8..0314c65fae 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -4,6 +4,7 @@ 'lib/underscore/underscore-min.js', 'lib/jquery-ui/jquery-ui.min.js', + 'lib/jquery-ui-touch-punch/jquery.ui.touch-punch.js', 'lib/angular/1.1.5/angular-cookies.min.js', 'lib/angular/1.1.5/angular-mobile.js', @@ -12,17 +13,13 @@ 'lib/angular/angular-ui-sortable.js', 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', - - 'lib/blueimp-load-image/load-image.all.min.js', - 'lib/jquery-file-upload/jquery.fileupload.js', - 'lib/jquery-file-upload/jquery.fileupload-process.js', - 'lib/jquery-file-upload/jquery.fileupload-image.js', - 'lib/jquery-file-upload/jquery.fileupload-angular.js', + 'lib/ng-file-upload/ng-file-upload.min.js', + 'lib/angular-local-storage/angular-local-storage.min.js', 'lib/bootstrap/js/bootstrap.2.3.2.min.js', 'lib/bootstrap-tabdrop/bootstrap-tabdrop.js', 'lib/umbraco/Extensions.js', - + 'lib/umbraco/NamespaceManager.js', 'lib/umbraco/LegacyUmbClientMgr.js', 'lib/umbraco/LegacySpeechBubble.js', @@ -37,4 +34,4 @@ 'js/umbraco.controllers.js', 'js/routes.js', 'js/init.js' -] \ No newline at end of file +] diff --git a/src/Umbraco.Web/UI/Pages/ClientTools.cs b/src/Umbraco.Web/UI/Pages/ClientTools.cs index fe7205451f..44464676a4 100644 --- a/src/Umbraco.Web/UI/Pages/ClientTools.cs +++ b/src/Umbraco.Web/UI/Pages/ClientTools.cs @@ -6,6 +6,7 @@ using Umbraco.Core.IO; using umbraco.BasePages; using System.Web.UI; using umbraco.BusinessLogic; +using Umbraco.Core; namespace Umbraco.Web.UI.Pages { @@ -47,7 +48,12 @@ namespace Umbraco.Web.UI.Pages public static string ChangeContentFrameUrl(string url) { return string.Format(ClientMgrScript + ".contentFrame('{0}');", url); } - public static string ChildNodeCreated = GetMainTree + ".childNodeCreated();"; + public static string ReloadContentFrameUrlIfPathLoaded(string url) + { + return string.Format(ClientMgrScript + ".reloadContentFrameUrlIfPathLoaded('{0}');", url); + } + public static string ReloadLocation { get { return string.Format(ClientMgrScript + ".reloadLocation();"); } } + public static string ChildNodeCreated = GetMainTree + ".childNodeCreated();"; public static string SyncTree { get { return GetMainTree + ".syncTree('{0}', {1});"; } } public static string ClearTreeCache { get { return GetMainTree + ".clearTreeCache();"; } } public static string CopyNode { get { return GetMainTree + ".copyNode('{0}', '{1}');"; } } @@ -146,23 +152,57 @@ namespace Umbraco.Web.UI.Pages //don't load if there is no url if (string.IsNullOrEmpty(url)) return this; - if (url.StartsWith("/") && !url.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco))) - url = IOHelper.ResolveUrl(SystemDirectories.Umbraco) + "/" + url; - - if (url.Trim().StartsWith("~")) - url = IOHelper.ResolveUrl(url); + url = EnsureUmbracoUrl(url); RegisterClientScript(Scripts.ChangeContentFrameUrl(url)); return this; } - /// - /// Shows the dashboard for the given application - /// - /// - /// - public ClientTools ShowDashboard(string app) + /// + /// Reloads the content in the content frame if the specified URL is currently loaded + /// + /// + public ClientTools ReloadContentFrameUrlIfPathLoaded(string url) + { + if (string.IsNullOrEmpty(url)) return this; + + url = EnsureUmbracoUrl(url); + + RegisterClientScript(Scripts.ReloadContentFrameUrlIfPathLoaded(url)); + + return this; + } + + /// + /// Reloads location, refreshing what is in the content frame + /// + public ClientTools ReloadLocation() + { + RegisterClientScript(Scripts.ReloadLocation); + + return this; + } + + private string EnsureUmbracoUrl(string url) + { + if (url.StartsWith("/") && url.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false) + { + url = IOHelper.ResolveUrl(SystemDirectories.Umbraco).EnsureEndsWith('/') + url; + } + + if (url.Trim().StartsWith("~")) + url = IOHelper.ResolveUrl(url); + + return url; + } + + /// + /// Shows the dashboard for the given application + /// + /// + /// + public ClientTools ShowDashboard(string app) { return ChangeContentFrameUrl(SystemDirectories.Umbraco + string.Format("/dashboard.aspx?app={0}", app)); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 39b693af90..de4b49bc77 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -99,14 +99,17 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll + False True ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll + False True ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll + False True @@ -118,6 +121,7 @@ ..\packages\Examine.0.1.68.0\lib\Examine.dll + False True @@ -132,6 +136,11 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Markdown.1.14.4\lib\net45\MarkdownSharp.dll + False + True + False ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll @@ -162,8 +171,9 @@ ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll - True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + False + True False @@ -171,6 +181,7 @@ ..\packages\Newtonsoft.Json.6.0.8\lib\net45\Newtonsoft.Json.dll + False True @@ -214,6 +225,7 @@ ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll + False True @@ -230,6 +242,7 @@ ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll + False True @@ -237,14 +250,17 @@ ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll + False True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll + False True ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll + False True @@ -286,9 +302,10 @@ {D7636876-0756-43CB-A192-138C6F0D5E42} umbraco.providers - + + ..\packages\UrlRewritingNet.UrlRewriter.2.0.7\lib\UrlRewritingNet.UrlRewriter.dll False - ..\packages\UrlRewritingNet.UrlRewriter.2.0.60829.1\lib\UrlRewritingNet.UrlRewriter.dll + False @@ -299,20 +316,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -569,7 +636,6 @@ - @@ -596,9 +662,6 @@ - - ASPXCodeBehind - ASPXCodeBehind @@ -639,6 +702,7 @@ + @@ -826,9 +890,6 @@ ASPXCodeBehind - - ASPXCodeBehind - ASPXCodeBehind @@ -891,6 +952,8 @@ + + @@ -1063,9 +1126,6 @@ ASPXCodeBehind - - ASPXCodeBehind - ASPXCodeBehind @@ -1243,7 +1303,6 @@ UploadMediaImage.ascx - @@ -1266,20 +1325,11 @@ Code - - - - - - - - - @@ -1380,37 +1430,9 @@ SendPublish.aspx - - content.ascx - ASPXCodeBehind - - - content.ascx - Code - - media.ascx - ASPXCodeBehind - - - media.ascx - - - member.ascx - ASPXCodeBehind - - - member.ascx - - - nodeType.ascx - ASPXCodeBehind - - - nodeType.ascx - Code @@ -1580,13 +1602,6 @@ EditMemberGroup.aspx - - EditMemberType.aspx - ASPXCodeBehind - - - EditMemberType.aspx - search.aspx ASPXCodeBehind @@ -1677,13 +1692,6 @@ editLanguage.aspx - - EditMediaType.aspx - ASPXCodeBehind - - - EditMediaType.aspx - editScript.aspx ASPXCodeBehind @@ -1830,10 +1838,6 @@ - - webService.asmx - Component - CacheRefresher.asmx @@ -2080,14 +2084,6 @@ - - ASPXCodeBehind - - - - ASPXCodeBehind - - ASPXCodeBehind @@ -2102,9 +2098,6 @@ Form - - ASPXCodeBehind - Designer @@ -2112,14 +2105,10 @@ ASPXCodeBehind - - ASPXCodeBehind - ASPXCodeBehind - Form @@ -2217,7 +2206,6 @@ umbraco_org_umbraco_update_CheckForUpgrade - 11.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 1c688080ce..8d56a81084 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web /// public class UmbracoContext : DisposableObject, IDisposeOnRequestEnd { - private const string HttpContextItemName = "Umbraco.Web.UmbracoContext"; + internal const string HttpContextItemName = "Umbraco.Web.UmbracoContext"; private static readonly object Locker = new object(); private bool _replacing; @@ -125,6 +125,7 @@ namespace Umbraco.Web if (umbracoSettings == null) throw new ArgumentNullException("umbracoSettings"); if (urlProviders == null) throw new ArgumentNullException("urlProviders"); + //if there's already a singleton, and we're not replacing then there's no need to ensure anything if (UmbracoContext.Current != null) { if (replaceContext == false) @@ -132,6 +133,39 @@ namespace Umbraco.Web UmbracoContext.Current._replacing = true; } + var umbracoContext = CreateContext(httpContext, applicationContext, webSecurity, umbracoSettings, urlProviders, preview); + + //assign the singleton + UmbracoContext.Current = umbracoContext; + return UmbracoContext.Current; + } + + /// + /// Creates a standalone UmbracoContext instance + /// + /// + /// + /// + /// + /// + /// + /// + /// A new instance of UmbracoContext + /// + public static UmbracoContext CreateContext( + HttpContextBase httpContext, + ApplicationContext applicationContext, + WebSecurity webSecurity, + IUmbracoSettingsSection umbracoSettings, + IEnumerable urlProviders, + bool? preview) + { + if (httpContext == null) throw new ArgumentNullException("httpContext"); + if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + if (webSecurity == null) throw new ArgumentNullException("webSecurity"); + if (umbracoSettings == null) throw new ArgumentNullException("umbracoSettings"); + if (urlProviders == null) throw new ArgumentNullException("urlProviders"); + var umbracoContext = new UmbracoContext( httpContext, applicationContext, @@ -142,15 +176,15 @@ namespace Umbraco.Web // create the RoutingContext, and assign var routingContext = new RoutingContext( umbracoContext, - + //TODO: Until the new cache is done we can't really expose these to override/mock new Lazy>(() => ContentFinderResolver.Current.Finders), new Lazy(() => ContentLastChanceFinderResolver.Current.Finder), - + // create the nice urls provider // there's one per request because there are some behavior parameters that can be changed new Lazy( - () => new UrlProvider( + () => new UrlProvider( umbracoContext, umbracoSettings.WebRouting, urlProviders), @@ -159,9 +193,7 @@ namespace Umbraco.Web //assign the routing context back umbracoContext.RoutingContext = routingContext; - //assign the singleton - UmbracoContext.Current = umbracoContext; - return UmbracoContext.Current; + return umbracoContext; } /// @@ -434,14 +466,11 @@ namespace Umbraco.Web var request = GetRequestFromContext(); if (request == null || request.Url == null) return false; - - var currentUrl = request.Url.AbsolutePath; - // zb-00004 #29956 : refactor cookies names & handling + return - //StateHelper.Cookies.Preview.HasValue // has preview cookie HttpContext.Request.HasPreviewCookie() - && currentUrl.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false - && UmbracoUser != null; // has user + && request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false + && Security.CurrentUser != null; // has user } private HttpRequestBase GetRequestFromContext() @@ -460,18 +489,9 @@ namespace Umbraco.Web protected override void DisposeResources() { Security.DisposeIfDisposable(); - Security = null; - _umbracoContext = null; - //ensure not to dispose this! - Application = null; - //Before we set these to null but in fact these are application lifespan singletons so - //there's no reason we need to set them to null and this also caused a problem with packages - //trying to access the cache properties on RequestEnd. - //http://issues.umbraco.org/issue/U4-2734 - //http://our.umbraco.org/projects/developer-tools/301-url-tracker/version-2/44327-Issues-with-URL-Tracker-in-614 - //ContentCache = null; - //MediaCache = null; + //If not running in a web ctx, ensure the thread based instance is nulled + _umbracoContext = null; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoContextExtensions.cs b/src/Umbraco.Web/UmbracoContextExtensions.cs index ec85b4dad6..8cd38df6d1 100644 --- a/src/Umbraco.Web/UmbracoContextExtensions.cs +++ b/src/Umbraco.Web/UmbracoContextExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Web; +using Umbraco.Core; using Umbraco.Core.Events; namespace Umbraco.Web @@ -11,6 +13,38 @@ namespace Umbraco.Web /// public static class UmbracoContextExtensions { + /// + /// tries to get the Umbraco context from the HttpContext + /// + /// + /// + /// + /// This is useful when working on async threads since the UmbracoContext is not copied over explicitly + /// + public static UmbracoContext GetUmbracoContext(this HttpContext http) + { + return GetUmbracoContext(new HttpContextWrapper(http)); + } + + /// + /// tries to get the Umbraco context from the HttpContext + /// + /// + /// + /// + /// This is useful when working on async threads since the UmbracoContext is not copied over explicitly + /// + public static UmbracoContext GetUmbracoContext(this HttpContextBase http) + { + if (http == null) throw new ArgumentNullException("http"); + + if (http.Items.Contains(UmbracoContext.HttpContextItemName)) + { + var umbCtx = http.Items[UmbracoContext.HttpContextItemName] as UmbracoContext; + return umbCtx; + } + return null; + } /// /// If there are event messages in the current request this will return them , otherwise it will return null diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 9aadb6fb42..046b2167d5 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -1,4 +1,7 @@ -using Microsoft.Owin; +using System; +using System.Web; +using Microsoft.Owin; +using Microsoft.Owin.Extensions; using Microsoft.Owin.Logging; using Owin; using Umbraco.Core; @@ -19,21 +22,62 @@ namespace Umbraco.Web /// public class UmbracoDefaultOwinStartup { + /// + /// Main startup method + /// + /// public virtual void Configuration(IAppBuilder app) + { + app.SanitizeThreadCulture(); + + ConfigureServices(app); + ConfigureMiddleware(app); + } + + /// + /// Configures services to be created in the OWIN context (CreatePerOwinContext) + /// + /// + protected virtual void ConfigureServices(IAppBuilder app) { app.SetUmbracoLoggerFactory(); //Configure the Identity user manager for use with Umbraco Back office // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( - ApplicationContext.Current, + ApplicationContext, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + } + /// + /// Configures middleware to be used (i.e. app.Use...) + /// + /// + protected virtual void ConfigureMiddleware(IAppBuilder app) + { //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN // cookie configuration, this must be declared after it. app - .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext.Current) - .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current); + .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) + .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) + .UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.Authorize) + .FinalizeMiddlewareConfiguration(); + } + + /// + /// Raised when the middelware has been configured + /// + public static event EventHandler MiddlewareConfigured; + + protected virtual ApplicationContext ApplicationContext + { + get { return ApplicationContext.Current; } + } + + internal static void OnMiddlewareConfigured(OwinMiddlewareConfiguredEventArgs args) + { + var handler = MiddlewareConfigured; + if (handler != null) handler(null, args); } } } diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 35797a4432..6d5e33b239 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -14,6 +14,9 @@ using Umbraco.Core.Xml; using Umbraco.Web.Routing; using Umbraco.Web.Security; using System.Collections.Generic; +using System.IO; +using System.Web.Mvc; +using System.Web.Routing; using Umbraco.Core.Cache; namespace Umbraco.Web @@ -1170,52 +1173,44 @@ namespace Umbraco.Web #region canvasdesigner - public HtmlString EnableCanvasDesigner() + [Obsolete("Use EnableCanvasDesigner on the HtmlHelper extensions instead")] + public IHtmlString EnableCanvasDesigner() { return EnableCanvasDesigner(string.Empty, string.Empty); } - public HtmlString EnableCanvasDesigner(string canvasdesignerConfigPath) + [Obsolete("Use EnableCanvasDesigner on the HtmlHelper extensions instead")] + public IHtmlString EnableCanvasDesigner(string canvasdesignerConfigPath) { return EnableCanvasDesigner(canvasdesignerConfigPath, string.Empty); } - public HtmlString EnableCanvasDesigner(string canvasdesignerConfigPath, string canvasdesignerPalettesPath) + [Obsolete("Use EnableCanvasDesigner on the HtmlHelper extensions instead")] + public IHtmlString EnableCanvasDesigner(string canvasdesignerConfigPath, string canvasdesignerPalettesPath) { + var html = CreateHtmlHelper(""); + var urlHelper = new UrlHelper(UmbracoContext.HttpContext.Request.RequestContext); + return html.EnableCanvasDesigner(urlHelper, UmbracoContext, canvasdesignerConfigPath, canvasdesignerPalettesPath); + } - string previewLink = @"" + - @"" + - @"" + - @"" + - @""; - - string noPreviewLinks = @""; - - // Get page value - int pageId = UmbracoContext.PublishedContentRequest.UmbracoPage.PageID; - string[] path = UmbracoContext.PublishedContentRequest.UmbracoPage.SplitPath; - string result = string.Empty; - string cssPath = CanvasDesignerUtility.GetStylesheetPath(path, false); - - if (UmbracoContext.Current.InPreviewMode) + [Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")] + private HtmlHelper CreateHtmlHelper(object model) + { + var cc = new ControllerContext { - canvasdesignerConfigPath = !string.IsNullOrEmpty(canvasdesignerConfigPath) ? canvasdesignerConfigPath : "/umbraco/js/canvasdesigner.config.js"; - canvasdesignerPalettesPath = !string.IsNullOrEmpty(canvasdesignerPalettesPath) ? canvasdesignerPalettesPath : "/umbraco/js/canvasdesigner.palettes.js"; - - if (!string.IsNullOrEmpty(cssPath)) - result = string.Format(noPreviewLinks, cssPath) + Environment.NewLine; + RequestContext = UmbracoContext.HttpContext.Request.RequestContext + }; + var viewContext = new ViewContext(cc, new FakeView(), new ViewDataDictionary(model), new TempDataDictionary(), new StringWriter()); + var htmlHelper = new HtmlHelper(viewContext, new ViewPage()); + return htmlHelper; + } - result = result + string.Format(previewLink, canvasdesignerConfigPath, canvasdesignerPalettesPath, pageId); - } - else + [Obsolete("This shouldn't need to be used but because the obsolete extension methods above don't have access to the current HtmlHelper, we need to create a fake one, unfortunately however this will not pertain the current views viewdata, tempdata or model state so should not be used")] + private class FakeView : IView + { + public void Render(ViewContext viewContext, TextWriter writer) { - // Get css path for current page - if (!string.IsNullOrEmpty(cssPath)) - result = string.Format(noPreviewLinks, cssPath); } - - return new HtmlString(result); - } #endregion diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 02bb83bd32..762a5f3e67 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -61,12 +61,12 @@ namespace Umbraco.Web legacyRequestInitializer.InitializeRequest(); // create the UmbracoContext singleton, one per request, and assign - // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) + // NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup) UmbracoContext.EnsureContext( - httpContext, - ApplicationContext.Current, - new WebSecurity(httpContext, ApplicationContext.Current), - true); + httpContext, + ApplicationContext.Current, + new WebSecurity(httpContext, ApplicationContext.Current), + true); } /// @@ -74,12 +74,12 @@ namespace Umbraco.Web /// /// /// - /// + /// /// This will check if we are trying to route to the default back office page (i.e. ~/Umbraco/ or ~/Umbraco or ~/Umbraco/Default ) - /// and ensure that the MVC handler executes for that. This is required because the route for /Umbraco will never execute because + /// and ensure that the MVC handler executes for that. This is required because the route for /Umbraco will never execute because /// files/folders exist there and we cannot set the RouteCollection.RouteExistingFiles = true since that will muck a lot of other things up. /// So we handle it here and explicitly execute the MVC controller. - /// + /// /// void ProcessRequest(HttpContextBase httpContext) { @@ -99,7 +99,7 @@ namespace Umbraco.Web { if (EnsureIsConfigured(httpContext, umbracoContext.OriginalRequestUrl)) { - RewriteToBackOfficeHandler(httpContext); + RewriteToBackOfficeHandler(httpContext); } return; } @@ -119,7 +119,7 @@ namespace Umbraco.Web { return; } - + httpContext.Trace.Write("UmbracoModule", "Umbraco request confirmed"); @@ -150,74 +150,9 @@ namespace Umbraco.Web #region Methods - /// - /// Determines if we should authenticate the request - /// - /// - /// - /// - /// - /// We auth the request when: - /// * it is a back office request - /// * it is an installer request - /// * it is a /base request - /// * it is a preview request - /// - internal static bool ShouldAuthenticateRequest(HttpRequestBase request, Uri originalRequestUrl) - { - if (//check back office - request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) - //check installer - || request.Url.IsInstallerRequest() - //detect in preview - || (request.HasPreviewCookie() && request.Url != null && request.Url.AbsolutePath.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false) - //check for base - || BaseRest.BaseRestHandler.IsBaseRestRequest(originalRequestUrl)) - { - return true; - } - return false; - } - - //private static readonly ConcurrentHashSet IgnoreTicketRenewUrls = new ConcurrentHashSet(); - ///// - ///// Determines if the authentication ticket should be renewed with a new timeout - ///// - ///// - ///// - ///// - ///// - ///// We do not want to renew the ticket when we are checking for the user's remaining timeout unless - - ///// UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn == true - ///// - //internal static bool ShouldIgnoreTicketRenew(Uri url, HttpContextBase httpContext) - //{ - // //this setting will renew the ticket for all requests. - // if (UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn) - // { - // return false; - // } - - // //initialize the ignore ticket urls - we don't need to lock this, it's concurrent and a hashset - // // we don't want to have to gen the url each request so this will speed things up a teeny bit. - // if (IgnoreTicketRenewUrls.Any() == false) - // { - // var urlHelper = new UrlHelper(new RequestContext(httpContext, new RouteData())); - // var checkSessionUrl = urlHelper.GetUmbracoApiService(controller => controller.GetRemainingTimeoutSeconds()); - // IgnoreTicketRenewUrls.Add(checkSessionUrl); - // } - - // if (IgnoreTicketRenewUrls.Any(x => url.AbsolutePath.StartsWith(x))) - // { - // return true; - // } - - // return false; - //} - /// /// Checks the current request and ensures that it is routable based on the structure of the request and URI - /// + /// /// /// /// @@ -236,12 +171,12 @@ namespace Umbraco.Web else if (!EnsureIsReady(httpContext, uri)) { reason = EnsureRoutableOutcome.NotReady; - } + } // ensure Umbraco is properly configured to serve documents else if (!EnsureIsConfigured(httpContext, uri)) { reason = EnsureRoutableOutcome.NotConfigured; - } + } // ensure Umbraco has documents to serve else if (!EnsureHasContent(context, httpContext)) { @@ -285,7 +220,8 @@ namespace Umbraco.Web // if the path contains an extension that is not .aspx // then it cannot be a document request - if (maybeDoc && lpath.Contains('.') && !lpath.EndsWith(".aspx")) + var extension = Path.GetExtension(lpath); + if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx") maybeDoc = false; // at that point, either we have no extension, or it is .aspx @@ -326,7 +262,7 @@ namespace Umbraco.Web httpContext.Response.StatusCode = 503; var bootUrl = "~/config/splashes/booting.aspx"; - + httpContext.RewritePath(UriUtility.ToAbsolute(bootUrl) + "?url=" + HttpUtility.UrlEncode(uri.ToString())); return false; @@ -345,7 +281,7 @@ namespace Umbraco.Web return true; LogHelper.Warn("Umbraco has no content"); - + const string noContentUrl = "~/config/splashes/noNodes.aspx"; httpContext.RewritePath(UriUtility.ToAbsolute(noContentUrl)); @@ -402,7 +338,7 @@ namespace Umbraco.Web response.TrySkipIisCustomErrors = UmbracoConfig.For.UmbracoSettings().WebRouting.TrySkipIisCustomErrors; if (response.TrySkipIisCustomErrors == false) - LogHelper.Warn("Status code is 404 yet TrySkipIisCustomErrors is false - IIS will take over."); + LogHelper.Warn("Status code is 404 yet TrySkipIisCustomErrors is false - IIS will take over."); } if (pcr.ResponseStatusCode > 0) @@ -438,8 +374,8 @@ namespace Umbraco.Web // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) context.RewritePath(rewritePath, "", "", false); - //if it is MVC we need to do something special, we are not using TransferRequest as this will - //require us to rewrite the path with query strings and then reparse the query strings, this would + //if it is MVC we need to do something special, we are not using TransferRequest as this will + //require us to rewrite the path with query strings and then reparse the query strings, this would //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: // * Looks up the route based on the new rewritten URL @@ -454,7 +390,7 @@ namespace Umbraco.Web /// /// Rewrites to the Umbraco handler - we always send the request via our MVC rendering engine, this will deal with /// requests destined for webforms. - /// + /// /// /// private static void RewriteToUmbracoHandler(HttpContextBase context, PublishedContentRequest pcr) @@ -471,8 +407,8 @@ namespace Umbraco.Web // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) context.RewritePath(rewritePath, "", query, false); - //if it is MVC we need to do something special, we are not using TransferRequest as this will - //require us to rewrite the path with query strings and then reparse the query strings, this would + //if it is MVC we need to do something special, we are not using TransferRequest as this will + //require us to rewrite the path with query strings and then reparse the query strings, this would //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: // * Looks up the route based on the new rewritten URL @@ -484,7 +420,7 @@ namespace Umbraco.Web urlRouting.PostResolveRequestCache(context); } - + /// /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request /// @@ -496,7 +432,7 @@ namespace Umbraco.Web return; //get a list of keys to dispose - var keys = new HashSet(); + var keys = new HashSet(); foreach (DictionaryEntry i in http.Items) { if (i.Value is IDisposeOnRequestEnd || i.Key is IDisposeOnRequestEnd) @@ -531,7 +467,7 @@ namespace Umbraco.Web #region IHttpModule /// - /// Initialize the module, this will trigger for each new application + /// Initialize the module, this will trigger for each new application /// and there may be more than 1 application per application domain /// /// @@ -545,7 +481,7 @@ namespace Umbraco.Web }; //disable asp.net headers (security) - // This is the correct place to modify headers according to MS: + // This is the correct place to modify headers according to MS: // https://our.umbraco.org/forum/umbraco-7/using-umbraco-7/65241-Heap-error-from-header-manipulation?p=0#comment220889 app.PostReleaseRequestState += (sender, args) => { @@ -555,6 +491,8 @@ namespace Umbraco.Web httpContext.Response.Headers.Remove("Server"); //this doesn't normally work since IIS sets it but we'll keep it here anyways. httpContext.Response.Headers.Remove("X-Powered-By"); + httpContext.Response.Headers.Remove("X-AspNet-Version"); + httpContext.Response.Headers.Remove("X-AspNetMvc-Version"); } catch (PlatformNotSupportedException ex) { @@ -570,7 +508,7 @@ namespace Umbraco.Web app.EndRequest += (sender, args) => { - var httpContext = ((HttpApplication)sender).Context; + var httpContext = ((HttpApplication)sender).Context; if (UmbracoContext.Current != null && UmbracoContext.Current.IsFrontEndUmbracoRequest) { LogHelper.Debug( @@ -604,7 +542,7 @@ namespace Umbraco.Web { if (EndRequest != null) EndRequest(this, args); - } + } #endregion @@ -636,7 +574,7 @@ namespace Umbraco.Web } return allRoutes; - }); + }); /// /// This is used internally to track any registered callback paths for Identity providers. If the request path matches diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index c42b46fa41..629d69ed4a 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -1,7 +1,10 @@ using System; using System.Linq; +using System.Web; using System.Web.Mvc; using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models; using Umbraco.Web.Mvc; namespace Umbraco.Web @@ -12,6 +15,209 @@ namespace Umbraco.Web public static class UrlHelperRenderExtensions { + #region GetCropUrl + + /// + /// Gets the ImageProcessor Url of a media item by the crop alias (using default media item property alias of "umbracoFile") + /// + /// + /// + /// The IPublishedContent item. + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string cropAlias, bool htmlEncode = true) + { + var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + /// + /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// + /// + /// + /// The IPublishedContent item. + /// + /// + /// The property alias of the property containing the Json data e.g. umbracoFile + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + /// The ImageProcessor.Web Url. + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true) + { + var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// Property alias of the property containing the Json data. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// The further options. + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + /// The . + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + IPublishedContent mediaItem, + int? width = null, + int? height = null, + string propertyAlias = Umbraco.Core.Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The Json data from the Umbraco Core Image Cropper property editor + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// The further options. + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// + /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be + /// set to false if using the result of this method for CSS. + /// + /// + /// The . + /// + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + string imageUrl, + int? width = null, + int? height = null, + string imageCropperValue = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + var url = imageUrl.GetCropUrl(width, height, imageCropperValue, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + #endregion + /// /// Generates a URL based on the current Umbraco URL with a custom query string that will route to the specified SurfaceController /// diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs index b5510ec85d..dda19f9ff9 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs @@ -162,7 +162,7 @@ namespace Umbraco.Web.WebApi.Binders //finally, let's lookup the real content item and create the DTO item model.PersistedContent = GetExisting(model); } - + //create the dto from the persisted model if (model.PersistedContent != null) { @@ -173,7 +173,9 @@ namespace Umbraco.Web.WebApi.Binders //now map all of the saved values to the dto MapPropertyValuesFromSaved(model, model.ContentDto); } - + + model.Name = model.Name.Trim(); + return model; } diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs index d48cd66077..962183f7ef 100644 --- a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -28,6 +30,8 @@ namespace Umbraco.Web.WebApi.Filters /// public const string AngularHeadername = "X-XSRF-TOKEN"; + + /// /// Returns 2 tokens - one for the cookie value and one that angular should set as the header value /// @@ -64,13 +68,10 @@ namespace Umbraco.Web.WebApi.Filters return true; } - /// - /// Validates the headers/cookies passed in for the request - /// - /// - /// - /// - public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason) + internal static bool ValidateHeaders( + KeyValuePair>[] requestHeaders, + string cookieToken, + out string failedReason) { failedReason = ""; @@ -85,12 +86,7 @@ namespace Umbraco.Web.WebApi.Filters .Select(z => z.Value) .SelectMany(z => z) .FirstOrDefault(); - - var cookieToken = requestHeaders - .GetCookies() - .Select(c => c[CsrfValidationCookieName]) - .FirstOrDefault(); - + // both header and cookie must be there if (cookieToken == null || headerToken == null) { @@ -98,13 +94,32 @@ namespace Umbraco.Web.WebApi.Filters return false; } - if (ValidateTokens(cookieToken.Value, headerToken) == false) + if (ValidateTokens(cookieToken, headerToken) == false) { failedReason = "Invalid token"; return false; } - + return true; } + + /// + /// Validates the headers/cookies passed in for the request + /// + /// + /// + /// + public static bool ValidateHeaders(HttpRequestHeaders requestHeaders, out string failedReason) + { + var cookieToken = requestHeaders + .GetCookies() + .Select(c => c[CsrfValidationCookieName]) + .FirstOrDefault(); + + return ValidateHeaders( + requestHeaders.ToDictionary(x => x.Key, x => x.Value).ToArray(), + cookieToken == null ? null : cookieToken.Value, + out failedReason); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs index c526317d95..99e08a4597 100644 --- a/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/DisableBrowserCacheAttribute.cs @@ -1,18 +1,12 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Http.Controllers; using System.Web.Http.Filters; -using Umbraco.Core; namespace Umbraco.Web.WebApi.Filters { + /// + /// Ensures that the request is not cached by the browser + /// public class DisableBrowserCacheAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) @@ -20,35 +14,29 @@ namespace Umbraco.Web.WebApi.Filters //See: http://stackoverflow.com/questions/17755239/how-to-stop-chrome-from-caching-rest-response-from-webapi base.OnActionExecuted(actionExecutedContext); - - //TODO: This should all work without issue! BUT it doesn't, i have a feeling this might be fixed - // in the next webapi version. ASP.Net is overwriting the cachecontrol all the time, some docs are here: - // http://stackoverflow.com/questions/11547618/output-caching-for-an-apicontroller-mvc4-web-api - // and I've checked the source code so doing this should cause it to write the headers we want but it doesnt. - //So I've reverted to brute force on the HttpContext. - //actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - //{ - // NoCache = true, - // NoStore = true, - // MaxAge = new TimeSpan(0), - // MustRevalidate = true - //}; - - HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache); - HttpContext.Current.Response.Cache.SetMaxAge(TimeSpan.Zero); - HttpContext.Current.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); - HttpContext.Current.Response.Cache.SetNoStore(); + if (actionExecutedContext == null || actionExecutedContext.Response == null || + actionExecutedContext.Response.Headers == null) + { + return; + } + //NOTE: Until we upgraded to WebApi 2, this didn't work correctly and we had to revert to using + // HttpContext.Current responses. I've changed this back to what it should be now since it works + // and now with WebApi2, the HttpContext.Current responses dont! Anyways, all good now. + actionExecutedContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + NoCache = true, + NoStore = true, + MaxAge = new TimeSpan(0), + MustRevalidate = true + }; actionExecutedContext.Response.Headers.Pragma.Add(new NameValueHeaderValue("no-cache")); if (actionExecutedContext.Response.Content != null) { actionExecutedContext.Response.Content.Headers.Expires = - //Mon, 01 Jan 1990 00:00:00 GMT - new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); + //Mon, 01 Jan 1990 00:00:00 GMT + new DateTimeOffset(1990, 1, 1, 0, 0, 0, TimeSpan.Zero); } - - - } } } diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs new file mode 100644 index 0000000000..ae10d073cd --- /dev/null +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs @@ -0,0 +1,38 @@ +using System; +using System.Net.Http; +using System.Web.Http.Filters; +using Umbraco.Core; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Used to emit outgoing editor model events + /// + internal sealed class OutgoingEditorModelEventAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext.Response == null) return; + + var user = UmbracoContext.Current.Security.CurrentUser; + if (user == null) return; + + var objectContent = actionExecutedContext.Response.Content as ObjectContent; + if (objectContent != null) + { + var model = objectContent.Value; + + if (model != null) + { + EditorModelEventManager.EmitEvent(actionExecutedContext, new EditorModelEventArgs( + (dynamic)model, + UmbracoContext.Current)); + } + } + + base.OnActionExecuted(actionExecutedContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs index 693d45c792..c42881f52a 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -1,23 +1,17 @@ -using System.Web.Http.Filters; +using System; +using System.ComponentModel; +using System.Web.Http.Filters; using Umbraco.Core.Security; namespace Umbraco.Web.WebApi.Filters { - /// - /// A filter that is used to remove the authorization cookie for the current user when the request is successful - /// - /// - /// This is used so that we can log a user OUT in conjunction with using other filters that modify the cookies collection. - /// SD: I beleive this is a bug with web api since if you modify the cookies collection on the HttpContext.Current and then - /// use a filter to write the cookie headers, the filter seems to have no affect at all. - /// + [Obsolete("This is no longer used and will be removed from the codebase in the future, use OWIN IAuthenticationManager.SignOut instead", true)] + [EditorBrowsable(EditorBrowsableState.Never)] public sealed class UmbracoBackOfficeLogoutAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext context) - { - if (context.Response == null) return; - if (context.Response.IsSuccessStatusCode == false) return; - context.Response.UmbracoLogoutWebApi(); + { + throw new NotSupportedException("This method is not supported and should not be used, it has been removed in Umbraco 7.4"); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs index bd2290ce17..04035c0017 100644 --- a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs +++ b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs @@ -11,6 +11,7 @@ using System.Web.Http; using System.Web.Http.ModelBinding; using Microsoft.Owin; using Umbraco.Core; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.WebApi { @@ -107,6 +108,22 @@ namespace Umbraco.Web.WebApi return msg; } + /// + /// Creates an error response with notifications in the result to be displayed in the UI + /// + /// + /// + /// + public static HttpResponseMessage CreateNotificationValidationErrorResponse(this HttpRequestMessage request, string errorMessage) + { + var notificationModel = new SimpleNotificationModel + { + Message = errorMessage + }; + notificationModel.AddErrorNotification(errorMessage, string.Empty); + return request.CreateValidationErrorResponse(notificationModel); + } + /// /// Create a 400 response message indicating that a validation error occurred /// diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs new file mode 100644 index 0000000000..c5563e6509 --- /dev/null +++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs @@ -0,0 +1,37 @@ +using System; +using System.Web.Http.Controllers; +using System.Web.Http.Metadata; +using System.Web.Http.Validation; + +namespace Umbraco.Web.WebApi +{ + /// + /// By default WebApi always appends a prefix to any ModelState error but we don't want this, + /// so this is a custom validator that ensures there is no prefix set. + /// + /// + /// We were already doing this with the content/media/members validation since we had to manually validate because we + /// were posting multi-part values. We were always passing in an empty prefix so it worked. However for other editors we + /// are validating with normal data annotations (for the most part) and we don't want the prefix there either. + /// + internal class PrefixlessBodyModelValidator : IBodyModelValidator + { + private readonly IBodyModelValidator _innerValidator; + + public PrefixlessBodyModelValidator(IBodyModelValidator innerValidator) + { + if (innerValidator == null) + { + throw new ArgumentNullException("innerValidator"); + } + + _innerValidator = innerValidator; + } + + public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix) + { + // Remove the keyPrefix but otherwise let innerValidator do what it normally does. + return _innerValidator.Validate(model, type, metadataProvider, actionContext, string.Empty); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs new file mode 100644 index 0000000000..e018881f8a --- /dev/null +++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Validation; + +namespace Umbraco.Web.WebApi +{ + /// + /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// + internal class PrefixlessBodyModelValidatorAttribute : Attribute, IControllerConfiguration + { + public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + //replace the normal validator with our custom one for this controller + controllerSettings.Services.Replace(typeof(IBodyModelValidator), + new PrefixlessBodyModelValidator(controllerSettings.Services.GetBodyModelValidator())); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index d93c070cf4..b850532011 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.WebApi /// The base class for auto-routed API controllers for Umbraco /// public abstract class UmbracoApiController : UmbracoApiControllerBase - { + { protected UmbracoApiController() { } @@ -20,5 +20,9 @@ namespace Umbraco.Web.WebApi protected UmbracoApiController(UmbracoContext umbracoContext) : base(umbracoContext) { } + + protected UmbracoApiController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } } } diff --git a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs index ad29726d4e..a4d09f626d 100644 --- a/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoAuthorizedApiController.cs @@ -18,18 +18,21 @@ namespace Umbraco.Web.WebApi [IsBackOffice] [UmbracoUserTimeoutFilter] [UmbracoAuthorize] + [DisableBrowserCache] public abstract class UmbracoAuthorizedApiController : UmbracoApiController { protected UmbracoAuthorizedApiController() { - } - protected UmbracoAuthorizedApiController(UmbracoContext umbracoContext) - : base(umbracoContext) + protected UmbracoAuthorizedApiController(UmbracoContext umbracoContext) : base(umbracoContext) { } - + + protected UmbracoAuthorizedApiController(UmbracoContext umbracoContext, UmbracoHelper umbracoHelper) : base(umbracoContext, umbracoHelper) + { + } + private bool _userisValidated = false; /// diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index baac1109e4..93928d0867 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration; using System.Linq; +using System.Reflection; using System.Web; using System.Web.Configuration; using System.Web.Http; @@ -12,6 +13,7 @@ using System.Web.Routing; using ClientDependency.Core.Config; using Examine; using Examine.Config; +using Examine.Providers; using umbraco; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -39,10 +41,12 @@ using Umbraco.Web.Scheduling; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; +using Umbraco.Core.Cache; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Publishing; using Umbraco.Core.Services; +using Umbraco.Web.Editors; using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; @@ -50,13 +54,13 @@ using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; namespace Umbraco.Web { /// - /// A bootstrapper for the Umbraco application which initializes all objects including the Web portion of the application + /// A bootstrapper for the Umbraco application which initializes all objects including the Web portion of the application /// public class WebBootManager : CoreBootManager { private readonly bool _isForTesting; //NOTE: see the Initialize method for what this is used for - private readonly List _indexesToRebuild = new List(); + private static readonly List IndexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : base(umbracoApplication) @@ -85,7 +89,7 @@ namespace Umbraco.Web protected override ServiceContext CreateServiceContext(DatabaseContext dbContext, IDatabaseFactory dbFactory) { //use a request based messaging factory - var evtMsgs = new RequestLifespanMessagesFactory(new SingletonUmbracoContextAccessor()); + var evtMsgs = new RequestLifespanMessagesFactory(new SingletonHttpContextAccessor()); return new ServiceContext( new RepositoryFactory(ApplicationCache, ProfilingLogger.Logger, dbContext.SqlSyntax, UmbracoConfig.For.UmbracoSettings()), new PetaPocoUnitOfWorkProvider(dbFactory), @@ -103,10 +107,10 @@ namespace Umbraco.Web public override IBootManager Initialize() { //This is basically a hack for this item: http://issues.umbraco.org/issue/U4-5976 - // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's - // event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build - // it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the - // boot process has completed. It's a hack but it works. + // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's + // event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build + // it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the + // boot process has completed. It's a hack but it works. ExamineManager.Instance.BuildingEmptyIndexOnStartup += OnInstanceOnBuildingEmptyIndexOnStartup; base.Initialize(); @@ -132,7 +136,7 @@ namespace Umbraco.Web ViewEngines.Engines.Add(new PluginViewEngine()); //set model binder - ModelBinders.Binders.Add(new KeyValuePair(typeof(RenderModel), new RenderModelBinder())); + ModelBinderProviders.BinderProviders.Add(new RenderModelBinder()); // is a provider ////add the profiling action filter //GlobalFilters.Filters.Add(new ProfilingActionFilter()); @@ -144,16 +148,18 @@ namespace Umbraco.Web { "compositeFileHandlerPath", ClientDependencySettings.Instance.CompositeFileHandlerPath } }); ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); - + + // Disable the X-AspNetMvc-Version HTTP Header + MvcHandler.DisableMvcResponseHeader = true; InstallHelper insHelper = new InstallHelper(UmbracoContext.Current); insHelper.DeleteLegacyInstaller(); return this; } - + /// - /// Override this method in order to ensure that the UmbracoContext is also created, this can only be + /// Override this method in order to ensure that the UmbracoContext is also created, this can only be /// created after resolution is frozen! /// protected override void FreezeResolution() @@ -167,8 +173,8 @@ namespace Umbraco.Web httpContext, ApplicationContext, new WebSecurity(httpContext, ApplicationContext), - UmbracoConfig.For.UmbracoSettings(), - UrlProviderResolver.Current.Providers, + UmbracoConfig.For.UmbracoSettings(), + UrlProviderResolver.Current.Providers, false); } @@ -178,11 +184,10 @@ namespace Umbraco.Web protected override void InitializeProfilerResolver() { base.InitializeProfilerResolver(); - //Set the profiler to be the web profiler ProfilerResolver.Current.SetProfiler(new WebProfiler()); } - + /// /// Ensure that the OnApplicationStarted methods of the IApplicationEvents are called /// @@ -193,6 +198,9 @@ namespace Umbraco.Web //Wrap viewengines in the profiling engine WrapViewEngines(ViewEngines.Engines); + //add global filters + ConfigureGlobalFilters(); + //set routes CreateRoutes(); @@ -200,14 +208,14 @@ namespace Umbraco.Web //Now, startup all of our legacy startup handler ApplicationEventsResolver.Current.InstantiateLegacyStartupHandlers(); - - //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them + + //Ok, now that everything is complete we'll check if we've stored any references to index that need rebuilding and run them // (see the initialize method for notes) - we'll ensure we remove the event handler too in case examine manager doesn't actually // initialize during startup, in which case we want it to rebuild the indexes itself. ExamineManager.Instance.BuildingEmptyIndexOnStartup -= OnInstanceOnBuildingEmptyIndexOnStartup; - if (_indexesToRebuild.Any()) + if (IndexesToRebuild.Any()) { - foreach (var indexer in _indexesToRebuild) + foreach (var indexer in IndexesToRebuild) { indexer.RebuildIndex(); } @@ -219,6 +227,11 @@ namespace Umbraco.Web return this; } + internal static void ConfigureGlobalFilters() + { + GlobalFilters.Filters.Add(new EnsurePartialViewMacroViewContextFilterAttribute()); + } + internal static void WrapViewEngines(IList viewEngines) { if (viewEngines == null || viewEngines.Count == 0) return; @@ -238,7 +251,19 @@ namespace Umbraco.Web protected override CacheHelper CreateApplicationCache() { //create a web-based cache helper - return new CacheHelper(); + var cacheHelper = new CacheHelper( + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new HttpRuntimeCacheProvider(HttpRuntime.Cache)), + new StaticCacheProvider(), + //we need request based cache when running in web-based context + new HttpRequestCacheProvider(), + new IsolatedRuntimeCache(type => + //we need to have the dep clone runtime cache provider to ensure + //all entities are cached properly (cloned in and cloned out) + new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + + return cacheHelper; } /// @@ -265,7 +290,7 @@ namespace Umbraco.Web //plugin controllers must come first because the next route will catch many things RoutePluginControllers(); } - + private void RoutePluginControllers() { var umbracoPath = GlobalSettings.UmbracoMvcArea; @@ -321,7 +346,7 @@ namespace Umbraco.Web { route.DataTokens = new RouteValueDictionary(); } - route.DataTokens.Add("umbraco", "api"); //ensure the umbraco token is set + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set } private void RouteLocalSurfaceController(Type controller, string umbracoPath) @@ -332,20 +357,22 @@ namespace Umbraco.Web umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}",//url to match new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, new[] { meta.ControllerNamespace }); //look in this namespace to create the controller - route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); //ensure the umbraco token is set route.DataTokens.Add("UseNamespaceFallback", false); //Don't look anywhere else except this namespace! //make it use our custom/special SurfaceMvcHandler route.RouteHandler = new SurfaceRouteHandler(); } /// - /// Initializes all web based and core resolves + /// Initializes all web based and core resolves /// protected override void InitializeResolvers() { base.InitializeResolvers(); - XsltExtensionsResolver.Current = new XsltExtensionsResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.Current.ResolveXsltExtensions()); + XsltExtensionsResolver.Current = new XsltExtensionsResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveXsltExtensions()); + + EditorValidationResolver.Current= new EditorValidationResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveTypes()); //set the default RenderMvcController DefaultRenderMvcControllerResolver.Current = new DefaultRenderMvcControllerResolver(typeof(RenderMvcController)); @@ -358,7 +385,7 @@ namespace Umbraco.Web //set the legacy one by default - this maintains backwards compat ServerMessengerResolver.Current.SetServerMessenger(new BatchedWebServiceServerMessenger(() => { - //we should not proceed to change this if the app/database is not configured since there will + //we should not proceed to change this if the app/database is not configured since there will // be no user, plus we don't need to have server messages sent if this is the case. if (ApplicationContext.IsConfigured && ApplicationContext.DatabaseContext.IsDatabaseConfigured) { @@ -386,58 +413,49 @@ namespace Umbraco.Web else { - // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed - // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty - // this callback is used below for the DatabaseServerMessenger startup options + //We are using a custom action here so we can check the examine settings value first, we don't want to + // put that check into the CreateIndexesOnColdBoot method because developers may choose to use this + // method directly and they will be in charge of this check if they need it Action rebuildIndexes = () => { - //If the developer has explicitly opted out of rebuilding indexes on startup then we + //If the developer has explicitly opted out of rebuilding indexes on startup then we // should adhere to that and not do it, this means that if they are load balancing things will be // out of sync if they are auto-scaling but there's not much we can do about that. if (ExamineSettings.Instance.RebuildOnAppStart == false) return; - if (_indexesToRebuild.Any()) + foreach (var indexer in GetIndexesForColdBoot()) { - var otherIndexes = ExamineManager.Instance.IndexProviderCollection.Except(_indexesToRebuild); - foreach (var otherIndex in otherIndexes) - { - otherIndex.RebuildIndex(); - } - } - else - { - //rebuild them all - ExamineManager.Instance.RebuildIndex(); + indexer.RebuildIndex(); } }; ServerMessengerResolver.Current.SetServerMessenger(new BatchedDatabaseServerMessenger( - ApplicationContext, - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] + ApplicationContext, + true, + //Default options for web including the required callbacks to build caches + new DatabaseServerMessengerOptions { - //rebuild the xml cache file if the server is not synced - () => global::umbraco.content.Instance.RefreshContentFromDatabase(), - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - rebuildIndexes - } - })); + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => global::umbraco.content.Instance.RefreshContentFromDatabase(), + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + rebuildIndexes + } + })); } SurfaceControllerResolver.Current = new SurfaceControllerResolver( ServiceProvider, LoggerResolver.Current.Logger, - PluginManager.Current.ResolveSurfaceControllers()); + PluginManager.ResolveSurfaceControllers()); UmbracoApiControllerResolver.Current = new UmbracoApiControllerResolver( ServiceProvider, LoggerResolver.Current.Logger, - PluginManager.Current.ResolveUmbracoApiControllers()); + PluginManager.ResolveUmbracoApiControllers()); // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be // discovered when CoreBootManager configures the converters. We HAVE to remove one of them @@ -448,12 +466,13 @@ namespace Umbraco.Web // same for other converters PropertyValueConvertersResolver.Current.RemoveType(); PropertyValueConvertersResolver.Current.RemoveType(); + PropertyValueConvertersResolver.Current.RemoveType(); PublishedCachesResolver.Current = new PublishedCachesResolver(new PublishedCaches( new PublishedCache.XmlPublishedCache.PublishedContentCache(), new PublishedCache.XmlPublishedCache.PublishedMediaCache(ApplicationContext))); - GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), + GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration)); FilteredControllerFactoriesResolver.Current = new FilteredControllerFactoriesResolver( @@ -506,22 +525,50 @@ namespace Umbraco.Web ThumbnailProvidersResolver.Current = new ThumbnailProvidersResolver( ServiceProvider, LoggerResolver.Current.Logger, - PluginManager.Current.ResolveThumbnailProviders()); + PluginManager.ResolveThumbnailProviders()); ImageUrlProviderResolver.Current = new ImageUrlProviderResolver( ServiceProvider, LoggerResolver.Current.Logger, - PluginManager.Current.ResolveImageUrlProviders()); + PluginManager.ResolveImageUrlProviders()); CultureDictionaryFactoryResolver.Current = new CultureDictionaryFactoryResolver( new DefaultCultureDictionaryFactory()); } + /// + /// The method used to create indexes on a cold boot + /// + /// + /// A cold boot is when the server determines it will not (or cannot) process instructions in the cache table and + /// will rebuild it's own caches itself. + /// + public static IEnumerable GetIndexesForColdBoot() + { + // NOTE: This is IMPORTANT! ... we don't want to rebuild any index that is already flagged to be re-indexed + // on startup based on our _indexesToRebuild variable and how Examine auto-rebuilds when indexes are empty. + // This callback is used above for the DatabaseServerMessenger startup options. + + // all indexes + IEnumerable indexes = ExamineManager.Instance.IndexProviderCollection; + + // except those that are already flagged + // and are processed in Complete() + if (IndexesToRebuild.Any()) + indexes = indexes.Except(IndexesToRebuild); + + // return + foreach (var index in indexes) + yield return index; + } + + private void OnInstanceOnBuildingEmptyIndexOnStartup(object sender, BuildingEmptyIndexOnStartupEventArgs args) { - //store the indexer that needs rebuilding because it's empty for when the boot process + //store the indexer that needs rebuilding because it's empty for when the boot process // is complete and cancel this current event so the rebuild process doesn't start right now. args.Cancel = true; - _indexesToRebuild.Add(args.Indexer); + IndexesToRebuild.Add((BaseIndexProvider)args.Indexer); } } } + diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index 30a609f6ff..cc9a153e07 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -15,6 +15,7 @@ namespace Umbraco.Web.WebServices /// /// A REST controller used for the publish dialog in order to publish bulk items at once /// + [ValidateMvcAngularAntiForgeryToken] public class BulkPublishController : UmbracoAuthorizedController { /// diff --git a/src/Umbraco.Web/WebServices/DomainsApiController.cs b/src/Umbraco.Web/WebServices/DomainsApiController.cs index 6446e0152d..30288fe8df 100644 --- a/src/Umbraco.Web/WebServices/DomainsApiController.cs +++ b/src/Umbraco.Web/WebServices/DomainsApiController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Web.Http; using System.Web.Services.Description; using Umbraco.Core; @@ -12,6 +13,7 @@ using Umbraco.Web.WebApi; //using umbraco.cms.businesslogic.language; using umbraco.BusinessLogic.Actions; using umbraco.cms.businesslogic.web; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.WebServices { @@ -19,6 +21,7 @@ namespace Umbraco.Web.WebServices /// A REST controller used for managing domains. /// /// Nothing to do with Active Directory. + [ValidateAngularAntiForgeryToken] public class DomainsApiController : UmbracoAuthorizedApiController { [HttpPost] @@ -52,28 +55,29 @@ namespace Umbraco.Web.WebServices if (language != null) { + // yet there is a race condition here... var wildcard = domains.FirstOrDefault(d => d.IsWildcard); if (wildcard != null) + { wildcard.LanguageId = language.Id; + } else { - // yet there is a race condition here... - var newDomain = new UmbracoDomain("*" + model.NodeId) + wildcard = new UmbracoDomain("*" + model.NodeId) { LanguageId = model.Language, RootContentId = model.NodeId }; - - var saveAttempt = Services.DomainService.Save(newDomain); - if (saveAttempt == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("Saving new domain failed"); - response.ReasonPhrase = saveAttempt.Result.StatusType.ToString(); - throw new HttpResponseException(response); - } } - + + var saveAttempt = Services.DomainService.Save(wildcard); + if (saveAttempt == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("Saving domain failed"); + response.ReasonPhrase = saveAttempt.Result.StatusType.ToString(); + throw new HttpResponseException(response); + } } else { @@ -82,7 +86,6 @@ namespace Umbraco.Web.WebServices { Services.DomainService.Delete(wildcard); } - } // process domains @@ -92,7 +95,7 @@ namespace Umbraco.Web.WebServices { Services.DomainService.Delete(domain); } - + var names = new List(); @@ -111,9 +114,30 @@ namespace Umbraco.Web.WebServices names.Add(name); var domain = domains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); if (domain != null) + { domain.LanguageId = language.Id; + Services.DomainService.Save(domain); + } else if (Services.DomainService.Exists(domainModel.Name)) + { domainModel.Duplicate = true; + var xdomain = Services.DomainService.GetByName(domainModel.Name); + var xrcid = xdomain.RootContentId; + if (xrcid.HasValue) + { + var xcontent = Services.ContentService.GetById(xrcid.Value); + var xnames = new List(); + while (xcontent != null) + { + xnames.Add(xcontent.Name); + if (xcontent.ParentId < -1) + xnames.Add("Recycle Bin"); + xcontent = xcontent.Parent(); + } + xnames.Reverse(); + domainModel.Other = "/" + string.Join("/", xnames); + } + } else { // yet there is a race condition here... @@ -130,7 +154,7 @@ namespace Umbraco.Web.WebServices response.ReasonPhrase = saveAttempt.Result.StatusType.ToString(); throw new HttpResponseException(response); } - } + } } model.Valid = model.Domains.All(m => m.Duplicate == false); @@ -159,6 +183,7 @@ namespace Umbraco.Web.WebServices public string Name { get; private set; } public int Lang { get; private set; } public bool Duplicate { get; set; } + public string Other { get; set; } } #endregion diff --git a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs index 136e2a984a..31ef4d23f2 100644 --- a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs +++ b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs @@ -13,9 +13,11 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web.Search; using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.WebServices { + [ValidateAngularAntiForgeryToken] public class ExamineManagementApiController : UmbracoAuthorizedApiController { /// @@ -258,9 +260,16 @@ namespace Umbraco.Web.WebServices //ignore these properties .Where(x => new[] {"IndexerData", "Description", "WorkingFolder"}.InvariantContains(x.Name) == false) .OrderBy(x => x.Name); + foreach (var p in props) { - indexerModel.ProviderProperties.Add(p.Name, p.GetValue(indexer, null).ToString()); + var val = p.GetValue(indexer, null); + if (val == null) + { + LogHelper.Warn("Property value was null when setting up property on indexer: " + indexer.Name + " property: " + p.Name); + val = string.Empty; + } + indexerModel.ProviderProperties.Add(p.Name, val.ToString()); } var luceneIndexer = indexer as LuceneIndexer; diff --git a/src/Umbraco.Web/WebServices/SaveFileController.cs b/src/Umbraco.Web/WebServices/SaveFileController.cs index 24f6917e76..871f1512ef 100644 --- a/src/Umbraco.Web/WebServices/SaveFileController.cs +++ b/src/Umbraco.Web/WebServices/SaveFileController.cs @@ -26,6 +26,7 @@ namespace Umbraco.Web.WebServices /// This isn't fully implemented yet but we should migrate all of the logic in the umbraco.presentation.webservices.codeEditorSave /// over to this controller. /// + [ValidateMvcAngularAntiForgeryToken] public class SaveFileController : UmbracoAuthorizedController { /// @@ -97,22 +98,28 @@ namespace Umbraco.Web.WebServices oldname = oldname.TrimStart(pathPrefix); } - var view = get(svce, oldname); - if (view == null) - view = new PartialView(filename); + var currentView = oldname.IsNullOrWhiteSpace() + ? get(svce, filename) + : get(svce, oldname); + + if (currentView == null) + currentView = new PartialView(filename); else - view.Path = filename; - view.Content = contents; + currentView.Path = filename; + currentView.Content = contents; + + + Attempt attempt; try { - var partialView = view as PartialView; + var partialView = currentView as PartialView; if (partialView != null && validate != null && validate(svce, partialView) == false) return Failed(ui.Text("speechBubbles", "partialViewErrorText"), ui.Text("speechBubbles", "partialViewErrorHeader"), - new FileSecurityException("File '" + view.Path + "' is not a valid partial view file.")); + new FileSecurityException("File '" + currentView.Path + "' is not a valid partial view file.")); - attempt = save(svce, view); + attempt = save(svce, currentView); } catch (Exception e) { @@ -125,7 +132,8 @@ namespace Umbraco.Web.WebServices attempt.Exception); } - return Success(ui.Text("speechBubbles", "partialViewSavedText"), ui.Text("speechBubbles", "partialViewSavedHeader")); + + return Success(ui.Text("speechBubbles", "partialViewSavedText"), ui.Text("speechBubbles", "partialViewSavedHeader"), new { name = currentView.Name, path = currentView.Path }); } /// @@ -148,8 +156,8 @@ namespace Umbraco.Web.WebServices { t = new Template(templateId) { - Text = templateName, - Alias = templateAlias, + Text = templateName.CleanForXss('[', ']', '(', ')', ':'), + Alias = templateAlias.CleanForXss('[', ']', '(', ')', ':'), Design = templateContents }; @@ -184,7 +192,8 @@ namespace Umbraco.Web.WebServices new { path = syncPath, - contents = t.Design + contents = t.Design, + alias = t.Alias // might have been updated! }); } catch (Exception ex) diff --git a/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs b/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs index 6c1859dbfa..66951d12f2 100644 --- a/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs +++ b/src/Umbraco.Web/WebServices/XmlDataIntegrityController.cs @@ -4,10 +4,11 @@ using Umbraco.Core; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.WebServices { - + [ValidateAngularAntiForgeryToken] public class XmlDataIntegrityController : UmbracoAuthorizedApiController { [HttpPost] diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index d255e152ba..2cf69b92b9 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -6,6 +6,7 @@ + @@ -26,6 +27,6 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 23e44896f4..1e013da452 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Text; @@ -7,6 +8,8 @@ using System.Threading; using System.Threading.Tasks; using System.Web; using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; using umbraco.BusinessLogic; using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.web; @@ -117,6 +120,7 @@ namespace umbraco private static readonly object DbReadSyncLock = new object(); private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; + private const string XmlContextClonedContentItemKey = "UmbracoXmlContextContent.cloned"; private static string _umbracoXmlDiskCacheFileName = string.Empty; private volatile XmlDocument _xmlContent; @@ -191,7 +195,7 @@ namespace umbraco // check if document *is* published, it could be unpublished by an event if (d.Published) { - var parentId = d.Level == 1 ? -1 : d.Parent.Id; + var parentId = d.Level == 1 ? -1 : d.ParentId; // fix sortOrder - see note in UpdateSortOrder var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); @@ -243,10 +247,6 @@ namespace umbraco /// The parent node identifier. public void SortNodes(int parentId) { - var childNodesXPath = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema - ? "./node" - : "./* [@id]"; - using (var safeXml = GetSafeXmlWriter(false)) { var parentNode = parentId == -1 @@ -257,7 +257,7 @@ namespace umbraco var sorted = XmlHelper.SortNodesIfNeeded( parentNode, - childNodesXPath, + ChildNodesXPath, x => x.AttributeValue("sortOrder")); if (sorted == false) return; @@ -297,7 +297,7 @@ namespace umbraco ClearContextCache(); var cachedFieldKeyStart = string.Format("{0}{1}_", CacheKeys.ContentItemCacheKey, d.Id); - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(cachedFieldKeyStart); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(cachedFieldKeyStart); FireAfterUpdateDocumentCache(d, e); } @@ -489,119 +489,76 @@ namespace umbraco { // Try to log to the DB LogHelper.Info("Loading content from database..."); - - var hierarchy = new Dictionary>(); - var nodeIndex = new Dictionary(); - + try { LogHelper.Debug("Republishing starting"); lock (DbReadSyncLock) { + //TODO: This is what we should do , but converting to use XDocument would be breaking unless we convert + // to XmlDocument at the end of this, but again, this would be bad for memory... though still not nearly as + // bad as what is happening before! + // We'll keep using XmlDocument for now though, but XDocument xml generation is much faster: + // https://blogs.msdn.microsoft.com/codejunkie/2008/10/08/xmldocument-vs-xelement-performance/ + // I think we already have code in here to convert XDocument to XmlDocument but in case we don't here + // it is: https://blogs.msdn.microsoft.com/marcelolr/2009/03/13/fast-way-to-convert-xmldocument-into-xdocument/ - // Lets cache the DTD to save on the DB hit on the subsequent use - string dtd = DocumentType.GenerateDtd(); + //// Prepare an XmlDocument with an appropriate inline DTD to match + //// the expected content + //var parent = new XElement("root", new XAttribute("id", "-1")); + //var xmlDoc = new XDocument( + // new XDocumentType("root", null, null, DocumentType.GenerateDtd()), + // parent); - // Prepare an XmlDocument with an appropriate inline DTD to match - // the expected content var xmlDoc = new XmlDocument(); - InitializeXml(xmlDoc, dtd); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); // Esben Carlsen: At some point we really need to put all data access into to a tier of its own. // CLN - added checks that document xml is for a document that is actually published. - string sql = - @"select umbracoNode.id, umbracoNode.parentId, umbracoNode.sortOrder, cmsContentXml.xml from umbracoNode + const string sql = @"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.xml, umbracoNode.level from umbracoNode inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) -order by umbracoNode.level, umbracoNode.sortOrder"; +order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; + XmlElement last = null; - - using ( - IRecordsReader dr = SqlHelper.ExecuteReader(sql, - SqlHelper.CreateParameter("@type", - new Guid( - Constants.ObjectTypes.Document))) - ) + var db = ApplicationContext.Current.DatabaseContext.Database; + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in db.Query(sql, new { type = new Guid(Constants.ObjectTypes.Document)})) { - while (dr.Read()) + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) { - int currentId = dr.GetInt("id"); - int parentId = dr.GetInt("parentId"); - string xml = dr.GetString("xml"); - - // fix sortOrder - see notes in UpdateSortOrder - var tmp = new XmlDocument(); - tmp.LoadXml(xml); - var attr = tmp.DocumentElement.GetAttributeNode("sortOrder"); - attr.Value = dr.GetInt("sortOrder").ToString(); - xml = tmp.InnerXml; - - // Call the eventhandler to allow modification of the string - var e1 = new ContentCacheLoadNodeEventArgs(); - FireAfterContentCacheDatabaseLoadXmlString(ref xml, e1); - // check if a listener has canceled the event - if (!e1.Cancel) - { - // and parse it into a DOM node - xmlDoc.LoadXml(xml); - XmlNode node = xmlDoc.FirstChild; - // same event handler loader form the xml node - var e2 = new ContentCacheLoadNodeEventArgs(); - FireAfterContentCacheLoadNodeFromDatabase(node, e2); - // and checking if it was canceled again - if (!e1.Cancel) - { - nodeIndex.Add(currentId, node); - - // verify if either of the handlers canceled the children to load - if (!e1.CancelChildren && !e2.CancelChildren) - { - // Build the content hierarchy - List children; - if (!hierarchy.TryGetValue(parentId, out children)) - { - // No children for this parent, so add one - children = new List(); - hierarchy.Add(parentId, children); - } - children.Add(currentId); - } - } - } + parent = xmlDoc.GetElementById(parentId); + if (parent == null) throw new InvalidOperationException("No parent node found in xml doc with id " + parentId); } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); } - LogHelper.Debug("Xml Pages loaded"); + LogHelper.Debug("Done republishing Xml Index"); - try - { - // If we got to here we must have successfully retrieved the content from the DB so - // we can safely initialise and compose the final content DOM. - // Note: We are reusing the XmlDocument used to create the xml nodes above so - // we don't have to import them into a new XmlDocument - - // Initialise the document ready for the final composition of content - InitializeXml(xmlDoc, dtd); - - // Start building the content tree recursively from the root (-1) node - GenerateXmlDocument(hierarchy, nodeIndex, -1, xmlDoc.DocumentElement); - - LogHelper.Debug("Done republishing Xml Index"); - - return xmlDoc; - } - catch (Exception ee) - { - LogHelper.Error("Error while generating XmlDocument from database", ee); - } + return xmlDoc; } - } - catch (OutOfMemoryException ee) - { - LogHelper.Error(string.Format("Error Republishing: Out Of Memory. Parents: {0}, Nodes: {1}", hierarchy.Count, nodeIndex.Count), ee); - } + } catch (Exception ee) { LogHelper.Error("Error Republishing", ee); @@ -617,64 +574,11 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return null; } - private static void GenerateXmlDocument(IDictionary> hierarchy, - IDictionary nodeIndex, int parentId, XmlNode parentNode) - { - List children; - - if (hierarchy.TryGetValue(parentId, out children)) - { - XmlNode childContainer = UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema || - String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME) - ? parentNode - : parentNode.SelectSingleNode( - UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME); - - if (!UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema && - !String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME)) - { - if (childContainer == null) - { - childContainer = xmlHelper.addTextNode(parentNode.OwnerDocument, - UmbracoSettings. - TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME, ""); - parentNode.AppendChild(childContainer); - } - } - - foreach (int childId in children) - { - XmlNode childNode = nodeIndex[childId]; - - if (UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema || - String.IsNullOrEmpty(UmbracoSettings.TEMP_FRIENDLY_XML_CHILD_CONTAINER_NODENAME)) - { - parentNode.AppendChild(childNode); - } - else - { - childContainer.AppendChild(childNode); - } - - // Recursively build the content tree under the current child - GenerateXmlDocument(hierarchy, nodeIndex, childId, childNode); - } - } - } - [Obsolete("This method should not be used and does nothing, xml file persistence is done in a queue using a BackgroundTaskRunner")] public void PersistXmlToFile() { } - - /// - /// Adds a task to the xml cache file persister - /// - //private void QueueXmlForPersistence() - //{ - // _persisterTask = _persisterTask.Touch(); - //} - + internal DateTime GetCacheFileUpdateTime() { //TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed? @@ -723,31 +627,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; private static bool UseLegacySchema { get { return UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema; } - } - - // whether to keep version of everything (incl. medias & members) in cmsPreviewXml - // for audit purposes - false by default, not in umbracoSettings.config - // whether to... no idea what that one does - // it is false by default and not in UmbracoSettings.config anymore - ignoring - /* - private static bool GlobalPreviewStorageEnabled - { - get { return UmbracoConfig.For.UmbracoSettings().Content.GlobalPreviewStorageEnabled; } - } - */ - - // ensures config is valid - private void EnsureConfigurationIsValid() - { - if (SyncToXmlFile && SyncFromXmlFile) - throw new Exception("Cannot run with both ContinouslyUpdateXmlDiskCache and XmlContentCheckForDiskChanges being true."); - - if (XmlIsImmutable == false) - //LogHelper.Warn("Running with CloneXmlContent being false is a bad idea."); - LogHelper.Warn("CloneXmlContent is false - ignored, we always clone."); - - // note: if SyncFromXmlFile then we should also disable / warn that local edits are going to cause issues... - } + } #endregion @@ -842,13 +722,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return xml2; } - private static void InitializeXml(XmlDocument xml, string dtd) - { - // prime the xml document with an inline dtd and a root element - xml.LoadXml(String.Format("{0}{1}{0}", - Environment.NewLine, dtd)); - } - // try to load from file, otherwise database // assumes xml lock (file is always locked) private void LoadXmlLocked(SafeXmlReaderWriter safeXml, out bool registerXmlChange) @@ -878,13 +751,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; return SafeXmlReaderWriter.GetReader(this, releaser); } - // gets a locked safe read accses to the main xml - private async Task GetSafeXmlReaderAsync() - { - var releaser = await _xmlLock.LockAsync(); - return SafeXmlReaderWriter.GetReader(this, releaser); - } - // gets a locked safe write access to the main xml (cloned) private SafeXmlReaderWriter GetSafeXmlWriter(bool auto = true) { @@ -965,8 +831,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; _releaser.Dispose(); _releaser = null; } + } - + private static string ChildNodesXPath { get @@ -1040,8 +907,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // save using (var fs = new FileStream(_xmlFileName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) { - var bytes = Encoding.UTF8.GetBytes(SaveXmlToString(xml)); - fs.Write(bytes, 0, bytes.Length); + SaveXmlToStream(xml, fs); } LogHelper.Info("Saved Xml to file."); @@ -1055,47 +921,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - // invoked by XmlCacheFilePersister ONLY and that one manages the MainDom, ie it - // will NOT try to save once the current app domain is not the main domain anymore - // (no need to test _released) - internal async Task SaveXmlToFileAsync() - { - LogHelper.Info("Save Xml to file..."); - - try - { - var xml = _xmlContent; // capture (atomic + volatile), immutable anyway - if (xml == null) return; - - // delete existing file, if any - DeleteXmlFile(); - - // ensure cache directory exists - var directoryName = Path.GetDirectoryName(_xmlFileName); - if (directoryName == null) - throw new Exception(string.Format("Invalid XmlFileName \"{0}\".", _xmlFileName)); - if (File.Exists(_xmlFileName) == false && Directory.Exists(directoryName) == false) - Directory.CreateDirectory(directoryName); - - // save - using (var fs = new FileStream(_xmlFileName, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) - { - var bytes = Encoding.UTF8.GetBytes(SaveXmlToString(xml)); - await fs.WriteAsync(bytes, 0, bytes.Length); - } - - LogHelper.Info("Saved Xml to file."); - } - catch (Exception e) - { - // if something goes wrong remove the file - DeleteXmlFile(); - - LogHelper.Error("Failed to save Xml to file.", e); - } - } - - private string SaveXmlToString(XmlDocument xml) + private void SaveXmlToStream(XmlDocument xml, Stream writeStream) { // using that one method because we want to have proper indent // and in addition, writing async is never fully async because @@ -1109,8 +935,12 @@ order by umbracoNode.level, umbracoNode.sortOrder"; // so ImportContent must also make sure of ignoring whitespaces! - var sb = new StringBuilder(); - using (var xmlWriter = XmlWriter.Create(sb, new XmlWriterSettings + if (writeStream.CanSeek) + { + writeStream.Position = 0; + } + + using (var xmlWriter = XmlWriter.Create(writeStream, new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8, @@ -1120,7 +950,6 @@ order by umbracoNode.level, umbracoNode.sortOrder"; //xmlWriter.WriteProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""); xml.WriteTo(xmlWriter); // already contains the xml declaration } - return sb.ToString(); } private XmlDocument LoadXmlFromFile() @@ -1302,6 +1131,11 @@ order by umbracoNode.level, umbracoNode.sortOrder"; publishedNode.Attributes.RemoveAll(); // remove all data nodes from the published node + //TODO: This could be faster, might as well just iterate all children and filter + // instead of selecting matching children (i.e. iterating all) and then iterating the + // filtered items to remove, this also allocates more memory to store the list of children. + // Below we also then do another filtering of child nodes, if we just iterate all children we + // can perform both functions more efficiently var dataNodes = publishedNode.SelectNodes(DataNodesXPath); if (dataNodes == null) throw new Exception("oops"); foreach (XmlNode n in dataNodes) @@ -1455,24 +1289,10 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - /// - /// Occurs when [after loading the xml string from the database]. - /// + [Obsolete("This is no used, do not use this for any reason")] + [EditorBrowsable(EditorBrowsableState.Never)] public static event ContentCacheDatabaseLoadXmlStringEventHandler AfterContentCacheDatabaseLoadXmlString; - /// - /// Fires the before when creating the document cache from database - /// - /// The sender. - /// The instance containing the event data. - internal static void FireAfterContentCacheDatabaseLoadXmlString(ref string xml, ContentCacheLoadNodeEventArgs e) - { - if (AfterContentCacheDatabaseLoadXmlString != null) - { - AfterContentCacheDatabaseLoadXmlString(ref xml, e); - } - } - /// /// Occurs when [before when creating the document cache from database]. /// @@ -1491,24 +1311,10 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } - /// - /// Occurs when [after loading document cache xml node from database]. - /// + [Obsolete("This is no used, do not use this for any reason")] + [EditorBrowsable(EditorBrowsableState.Never)] public static event ContentCacheLoadNodeEventHandler AfterContentCacheLoadNodeFromDatabase; - /// - /// Fires the after loading document cache xml node from database - /// - /// The sender. - /// The instance containing the event data. - internal static void FireAfterContentCacheLoadNodeFromDatabase(XmlNode node, ContentCacheLoadNodeEventArgs e) - { - if (AfterContentCacheLoadNodeFromDatabase != null) - { - AfterContentCacheLoadNodeFromDatabase(node, e); - } - } - /// /// Occurs when [before a publish action updates the content cache]. /// diff --git a/src/Umbraco.Web/umbraco.presentation/helper.cs b/src/Umbraco.Web/umbraco.presentation/helper.cs index f9292702e0..0d133fee66 100644 --- a/src/Umbraco.Web/umbraco.presentation/helper.cs +++ b/src/Umbraco.Web/umbraco.presentation/helper.cs @@ -112,14 +112,17 @@ namespace umbraco foreach (var attributeValueItem in attributeValueSplit) { attributeValue = attributeValueItem; + var trimmedValue = attributeValue.Trim(); // Check for special variables (always in square-brackets like [name]) - if (attributeValueItem.StartsWith("[") && - attributeValueItem.EndsWith("]")) + if (trimmedValue.StartsWith("[") && + trimmedValue.EndsWith("]")) { + attributeValue = trimmedValue; + // find key name - var keyName = attributeValueItem.Substring(2, attributeValueItem.Length - 3); - var keyType = attributeValueItem.Substring(1, 1); + var keyName = attributeValue.Substring(2, attributeValue.Length - 3); + var keyType = attributeValue.Substring(1, 1); switch (keyType) { diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 720e2ba9de..1edc6cca17 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -62,60 +62,51 @@ namespace umbraco else { // Loop through XML children we need to find the fields recursive - if (helper.FindAttribute(attributes, "recursive") == "true") + var recursive = helper.FindAttribute(attributes, "recursive") == "true"; + + if (publishedContent == null) { - if (publishedContent == null) - { - var recursiveVal = GetRecursiveValueLegacy(elements); - _fieldContent = recursiveVal.IsNullOrWhiteSpace() ? _fieldContent : recursiveVal; - } - else - { - var pval = publishedContent.GetPropertyValue(_fieldName, true); - var rval = pval == null ? string.Empty : pval.ToString(); - _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; - } + var recursiveVal = GetRecursiveValueLegacy(elements); + _fieldContent = recursiveVal.IsNullOrWhiteSpace() ? _fieldContent : recursiveVal; + } + + //check for published content and get its value using that + if (publishedContent != null && (publishedContent.HasProperty(_fieldName) || recursive)) + { + var pval = publishedContent.GetPropertyValue(_fieldName, recursive); + var rval = pval == null ? string.Empty : pval.ToString(); + _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; } else { - //check for published content and get its value using that - if (publishedContent != null && publishedContent.HasProperty(_fieldName)) - { - var pval = publishedContent.GetPropertyValue(_fieldName); - var rval = pval == null ? string.Empty : pval.ToString(); - _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; - } - else - { - //get the vaue the legacy way (this will not parse locallinks, etc... since that is handled with ipublishedcontent) - var elt = elements[_fieldName]; - if (elt != null && string.IsNullOrEmpty(elt.ToString()) == false) - _fieldContent = elt.ToString().Trim(); - } - - //now we check if the value is still empty and if so we'll check useIfEmpty - if (string.IsNullOrEmpty(_fieldContent)) - { - var altFieldName = helper.FindAttribute(attributes, "useIfEmpty"); - if (string.IsNullOrEmpty(altFieldName) == false) - { - if (publishedContent != null && publishedContent.HasProperty(altFieldName)) - { - var pval = publishedContent.GetPropertyValue(altFieldName); - var rval = pval == null ? string.Empty : pval.ToString(); - _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; - } - else - { - //get the vaue the legacy way (this will not parse locallinks, etc... since that is handled with ipublishedcontent) - var elt = elements[altFieldName]; - if (elt != null && string.IsNullOrEmpty(elt.ToString()) == false) - _fieldContent = elt.ToString().Trim(); - } - } - } - + //get the vaue the legacy way (this will not parse locallinks, etc... since that is handled with ipublishedcontent) + var elt = elements[_fieldName]; + if (elt != null && string.IsNullOrEmpty(elt.ToString()) == false) + _fieldContent = elt.ToString().Trim(); } + + //now we check if the value is still empty and if so we'll check useIfEmpty + if (string.IsNullOrEmpty(_fieldContent)) + { + var altFieldName = helper.FindAttribute(attributes, "useIfEmpty"); + if (string.IsNullOrEmpty(altFieldName) == false) + { + if (publishedContent != null && (publishedContent.HasProperty(altFieldName) || recursive)) + { + var pval = publishedContent.GetPropertyValue(altFieldName, recursive); + var rval = pval == null ? string.Empty : pval.ToString(); + _fieldContent = rval.IsNullOrWhiteSpace() ? _fieldContent : rval; + } + else + { + //get the vaue the legacy way (this will not parse locallinks, etc... since that is handled with ipublishedcontent) + var elt = elements[altFieldName]; + if (elt != null && string.IsNullOrEmpty(elt.ToString()) == false) + _fieldContent = elt.ToString().Trim(); + } + } + } + } ParseItem(attributes); diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index b9fac739f2..5047aef271 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -493,11 +493,11 @@ namespace umbraco { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) { - var xml = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var xml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( string.Format( "{0}_{1}_{2}", CacheKeys.MediaCacheKey, MediaId, Deep), - TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - () => GetMediaDo(MediaId, Deep)); + timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), + getCacheItem: () => GetMediaDo(MediaId, Deep)); if (xml != null) { @@ -552,11 +552,11 @@ namespace umbraco { if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) { - var xml = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var xml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( string.Format( "{0}_{1}", CacheKeys.MemberLibraryCacheKey, MemberId), - TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - () => GetMemberDo(MemberId)); + timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), + getCacheItem: () => GetMemberDo(MemberId)); if (xml != null) { @@ -602,7 +602,7 @@ namespace umbraco XmlDocument mXml = new XmlDocument(); mXml.LoadXml(m.ToXml(mXml, false).OuterXml); XPathNavigator xp = mXml.CreateNavigator(); - return xp.Select("/node"); + return xp.Select("/node()"); } XmlDocument xd = new XmlDocument(); @@ -1575,30 +1575,26 @@ namespace umbraco /// /// Sends an e-mail using the System.Net.Mail.MailMessage object /// - /// The sender of the e-mail - /// The recipient of the e-mail - /// E-mail subject - /// The complete content of the e-mail - /// Set to true when using Html formatted mails - public static void SendMail(string FromMail, string ToMail, string Subject, string Body, bool IsHtml) + /// The sender of the e-mail + /// The recipient(s) of the e-mail, add multiple email addresses by using a semicolon between them + /// E-mail subject + /// The complete content of the e-mail + /// Set to true when using Html formatted mails + public static void SendMail(string fromMail, string toMail, string subject, string body, bool isHtml) { try { - // create the mail message - MailMessage mail = new MailMessage(FromMail.Trim(), ToMail.Trim()); - - // populate the message - mail.Subject = Subject; - if (IsHtml) - mail.IsBodyHtml = true; - else - mail.IsBodyHtml = false; - - mail.Body = Body; - - // send it - SmtpClient smtpClient = new SmtpClient(); - smtpClient.Send(mail); + using (var mail = new MailMessage()) + { + mail.From = new MailAddress(fromMail.Trim()); + foreach (var mailAddress in toMail.Split(';')) + mail.To.Add(new MailAddress(mailAddress.Trim())); + mail.Subject = subject; + mail.IsBodyHtml = isHtml; + mail.Body = body; + using (var smtpClient = new SmtpClient()) + smtpClient.Send(mail); + } } catch (Exception ee) { diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 53bdaaa9de..77027749e2 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -24,6 +24,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Macros; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Core.Xml.XPath; using Umbraco.Core.Profiling; using umbraco.interfaces; @@ -62,7 +63,7 @@ namespace umbraco private readonly StringBuilder _content = new StringBuilder(); private const string MacrosAddedKey = "macrosAdded"; public IList Exceptions = new List(); - + protected static ISqlHelper SqlHelper { get { return Application.SqlHelper; } @@ -157,7 +158,7 @@ namespace umbraco public macro(string alias) { Macro m = Macro.GetByAlias(alias); - Model = new MacroModel(m); + Model = new MacroModel(m); } public MacroModel Model { get; set; } @@ -168,7 +169,7 @@ namespace umbraco } public static macro GetMacro(int id) - { + { return new macro(id); } @@ -199,9 +200,9 @@ namespace umbraco { if (this.Model != null) { - DistributedCache.Instance.RemoveMacroCache(this); + DistributedCache.Instance.RemoveMacroCache(this); } - + //this always returned false... hrm. oh well i guess we leave it like that return false; } @@ -253,7 +254,7 @@ namespace umbraco /// An event that is raised just before the macro is rendered allowing developers to modify the macro before it executes. /// public static event TypedEventHandler MacroRendering; - + /// /// Raises the MacroRendering event /// @@ -281,7 +282,7 @@ namespace umbraco using (DisposableTimer.DebugDuration(macroInfo)) { - TraceInfo("renderMacro", macroInfo, excludeProfiling:true); + TraceInfo("renderMacro", macroInfo, excludeProfiling: true); StateHelper.SetContextValue(MacrosAddedKey, StateHelper.GetContextValue(MacrosAddedKey) + 1); @@ -301,12 +302,13 @@ namespace umbraco { var renderFailed = false; var macroType = Model.MacroType != MacroTypes.Unknown - ? (int) Model.MacroType + ? (int)Model.MacroType : MacroType; + var textService = ApplicationContext.Current.Services.TextService; switch (macroType) { - case (int) MacroTypes.PartialView: + case (int)MacroTypes.PartialView: //error handler for partial views, is an action because we need to re-use it twice below Func handleError = e => @@ -322,12 +324,14 @@ namespace umbraco Exception = e, Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - return GetControlForErrorBehavior("Error loading Partial View script (file: " + ScriptFile + ")", macroErrorEventArgs); + + var errorMessage = textService.Localize("errors/macroErrorLoadingPartialView", new[] { ScriptFile }); + return GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); }; using (DisposableTimer.DebugDuration("Executing Partial View: " + Model.TypeName)) { - TraceInfo("umbracoMacro", "Partial View added (" + Model.TypeName + ")", excludeProfiling:true); + TraceInfo("umbracoMacro", "Partial View added (" + Model.TypeName + ")", excludeProfiling: true); try { var result = LoadPartialViewMacro(Model); @@ -362,13 +366,13 @@ namespace umbraco break; } - case (int) MacroTypes.UserControl: + case (int)MacroTypes.UserControl: using (DisposableTimer.DebugDuration("Executing UserControl: " + Model.TypeName)) { try { - TraceInfo("umbracoMacro", "Usercontrol added (" + Model.TypeName + ")", excludeProfiling:true); + TraceInfo("umbracoMacro", "Usercontrol added (" + Model.TypeName + ")", excludeProfiling: true); // Add tilde for v4 defined macros if (string.IsNullOrEmpty(Model.TypeName) == false && @@ -394,7 +398,8 @@ namespace umbraco Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - macroControl = GetControlForErrorBehavior("Error loading userControl '" + Model.TypeName + "'", macroErrorEventArgs); + var errorMessage = textService.Localize("errors/macroErrorLoadingUsercontrol", new[] { Model.TypeName }); + macroControl = GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); //if it is null, then we are supposed to throw the (original) exception // see: http://issues.umbraco.org/issue/U4-497 at the end if (macroControl == null) @@ -405,8 +410,8 @@ namespace umbraco break; } } - - case (int) MacroTypes.CustomControl: + + case (int)MacroTypes.CustomControl: using (DisposableTimer.DebugDuration("Executing CustomControl: " + Model.TypeName + "." + Model.TypeAssembly)) { @@ -435,7 +440,8 @@ namespace umbraco Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - macroControl = GetControlForErrorBehavior("Error loading customControl (Assembly: " + Model.TypeAssembly + ", Type: '" + Model.TypeName + "'", macroErrorEventArgs); + var errorMessage = textService.Localize("errors/macroErrorLoadingCustomControl", new[] { Model.TypeAssembly, Model.TypeName }); + macroControl = GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); //if it is null, then we are supposed to throw the (original) exception // see: http://issues.umbraco.org/issue/U4-497 at the end if (macroControl == null) @@ -446,10 +452,10 @@ namespace umbraco break; } } - case (int) MacroTypes.XSLT: + case (int)MacroTypes.XSLT: macroControl = LoadMacroXslt(this, Model, pageElements, true); - break; - case (int) MacroTypes.Script: + break; + case (int)MacroTypes.Script: //error handler for partial views, is an action because we need to re-use it twice below Func handleMacroScriptError = e => @@ -467,7 +473,8 @@ namespace umbraco Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - return GetControlForErrorBehavior("Error loading MacroEngine script (file: " + ScriptFile + ")", macroErrorEventArgs); + var errorMessage = textService.Localize("errors/macroErrorLoadingMacroEngineScript", new[] { ScriptFile }); + return GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); }; using (DisposableTimer.DebugDuration("Executing MacroEngineScript: " + ScriptFile)) @@ -505,7 +512,7 @@ namespace umbraco break; } } - case (int) MacroTypes.Unknown: + case (int)MacroTypes.Unknown: default: if (GlobalSettings.DebugMode) { @@ -564,11 +571,11 @@ namespace umbraco } //insert the cache string result - ApplicationContext.Current.ApplicationCache.InsertCacheItem( + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( CacheKeys.MacroHtmlCacheKey + Model.CacheIdentifier, - CacheItemPriority.NotRemovable, - new TimeSpan(0, 0, Model.CacheDuration), - () => outputCacheString); + priority: CacheItemPriority.NotRemovable, + timeout: new TimeSpan(0, 0, Model.CacheDuration), + getCacheItem: () => outputCacheString); dateAddedCacheKey = CacheKeys.MacroHtmlDateAddedCacheKey + Model.CacheIdentifier; @@ -583,11 +590,11 @@ namespace umbraco else { //insert the cache control result - ApplicationContext.Current.ApplicationCache.InsertCacheItem( + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( CacheKeys.MacroControlCacheKey + Model.CacheIdentifier, - CacheItemPriority.NotRemovable, - new TimeSpan(0, 0, Model.CacheDuration), - () => new MacroCacheContent(macroControl, macroControl.ID)); + priority: CacheItemPriority.NotRemovable, + timeout: new TimeSpan(0, 0, Model.CacheDuration), + getCacheItem: () => new MacroCacheContent(macroControl, macroControl.ID)); dateAddedCacheKey = CacheKeys.MacroControlDateAddedCacheKey + Model.CacheIdentifier; @@ -596,14 +603,14 @@ namespace umbraco } //insert the date inserted (so we can check file modification date) - ApplicationContext.Current.ApplicationCache.InsertCacheItem( + ApplicationContext.Current.ApplicationCache.RuntimeCache.InsertCacheItem( dateAddedCacheKey, - CacheItemPriority.NotRemovable, - new TimeSpan(0, 0, Model.CacheDuration), - () => DateTime.Now); - + priority: CacheItemPriority.NotRemovable, + timeout: new TimeSpan(0, 0, Model.CacheDuration), + getCacheItem: () => DateTime.Now); + } - + } } } @@ -633,7 +640,7 @@ namespace umbraco if (CacheMacroAsString(Model)) { - macroHtml = ApplicationContext.Current.ApplicationCache.GetCacheItem( + macroHtml = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( CacheKeys.MacroHtmlCacheKey + Model.CacheIdentifier); // FlorisRobbemont: @@ -659,7 +666,7 @@ namespace umbraco } else { - var cacheContent = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var cacheContent = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( CacheKeys.MacroControlCacheKey + Model.CacheIdentifier); if (cacheContent != null) @@ -723,7 +730,7 @@ namespace umbraco { if (MacroIsFileBased(model)) { - var cacheResult = ApplicationContext.Current.ApplicationCache.GetCacheItem(dateAddedKey); + var cacheResult = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem(dateAddedKey); if (cacheResult != null) { @@ -745,7 +752,7 @@ namespace umbraco switch (model.MacroType) { case MacroTypes.XSLT: - return string.Concat("~/xslt/", model.Xslt); + return string.Concat("~/xslt/", model.Xslt); case MacroTypes.Python: case MacroTypes.Script: return string.Concat("~/macroScripts/", model.ScriptName); @@ -753,7 +760,7 @@ namespace umbraco return model.ScriptName; //partial views are saved with the full virtual path case MacroTypes.UserControl: return model.TypeName; //user controls saved with the full virtual path - case MacroTypes.CustomControl: + case MacroTypes.CustomControl: case MacroTypes.Unknown: default: return "/" + model.TypeName; @@ -777,7 +784,7 @@ namespace umbraco { switch (model.MacroType) { - case MacroTypes.XSLT: + case MacroTypes.XSLT: case MacroTypes.Python: case MacroTypes.Script: case MacroTypes.PartialView: @@ -798,12 +805,12 @@ namespace umbraco CacheItemPriority.Default, new CacheDependency(IOHelper.MapPath(SystemDirectories.Xslt + "/" + XsltFile)), () => + { + using (var xslReader = new XmlTextReader(IOHelper.MapPath(SystemDirectories.Xslt.EnsureEndsWith('/') + XsltFile))) { - using (var xslReader = new XmlTextReader(IOHelper.MapPath(SystemDirectories.Xslt.EnsureEndsWith('/') + XsltFile))) - { - return CreateXsltTransform(xslReader, GlobalSettings.DebugMode); - } - }); + return CreateXsltTransform(xslReader, GlobalSettings.DebugMode); + } + }); } public void UpdateMacroModel(Hashtable attributes) @@ -857,7 +864,7 @@ namespace umbraco [Obsolete("This is no longer used in the codebase and will be removed in future versions")] public static void unloadXslt(string XsltFile) { - ApplicationContext.Current.ApplicationCache.ClearCacheByKeySearch(CacheKeys.MacroXsltCacheKey + XsltFile); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.MacroXsltCacheKey + XsltFile); } #region LoadMacroXslt @@ -884,7 +891,7 @@ namespace umbraco if (!canNavigate) { - // get master xml document + // get master xml document var cache = UmbracoContext.Current.ContentCache.InnerCache as Umbraco.Web.PublishedCache.XmlPublishedCache.PublishedContentCache; if (cache == null) throw new Exception("Unsupported IPublishedContentCache, only the Xml one is supported."); XmlDocument umbracoXml = cache.GetXml(UmbracoContext.Current, UmbracoContext.Current.InPreviewMode); @@ -916,7 +923,8 @@ namespace umbraco "

    " + HttpContext.Current.Server.HtmlEncode(outerXml) + "

    "); } - + + var textService = ApplicationContext.Current.Services.TextService; try { var xsltFile = getXslt(XsltFile); @@ -925,9 +933,9 @@ namespace umbraco { try { - var transformed = canNavigate - ? GetXsltTransformResult(macroNavigator, contentNavigator, xsltFile) // better? - : GetXsltTransformResult(macroXml, xsltFile); // document + var transformed = canNavigate + ? GetXsltTransformResult(macroNavigator, contentNavigator, xsltFile) // better? + : GetXsltTransformResult(macroXml, xsltFile); // document var result = CreateControlsFromText(transformed); return result; @@ -936,9 +944,11 @@ namespace umbraco { Exceptions.Add(e); LogHelper.WarnWithException("Error parsing XSLT file", e); - + var macroErrorEventArgs = new MacroErrorEventArgs { Name = Model.Name, Alias = Model.Alias, ItemKey = Model.Xslt, Exception = e, Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - var macroControl = GetControlForErrorBehavior("Error parsing XSLT file: \\xslt\\" + XsltFile, macroErrorEventArgs); + + var errorMessage = textService.Localize("errors/macroErrorParsingXSLTFile", new[] { XsltFile }); + var macroControl = GetControlForErrorBehavior(errorMessage, macroErrorEventArgs); //if it is null, then we are supposed to throw the (original) exception // see: http://issues.umbraco.org/issue/U4-497 at the end if (macroControl == null && throwError) @@ -946,8 +956,8 @@ namespace umbraco throw; } return macroControl; - } - } + } + } } catch (Exception e) { @@ -956,7 +966,8 @@ namespace umbraco // Invoke any error handlers for this macro var macroErrorEventArgs = new MacroErrorEventArgs { Name = Model.Name, Alias = Model.Alias, ItemKey = Model.Xslt, Exception = e, Behaviour = UmbracoConfig.For.UmbracoSettings().Content.MacroErrorBehaviour }; - var macroControl = GetControlForErrorBehavior("Error reading XSLT file: \\xslt\\" + XsltFile, macroErrorEventArgs); + var errorMessage = textService.Localize("errors/macroErrorReadingXSLTFile", new[] { XsltFile }); + var macroControl = GetControlForErrorBehavior(errorMessage + XsltFile, macroErrorEventArgs); //if it is null, then we are supposed to throw the (original) exception // see: http://issues.umbraco.org/issue/U4-497 at the end if (macroControl == null && throwError) @@ -965,7 +976,7 @@ namespace umbraco } return macroControl; } - } + } } // gets the control for the macro, using GetXsltTransform methods for execution @@ -1046,12 +1057,12 @@ namespace umbraco XsltArgumentList xslArgs; using (DisposableTimer.DebugDuration("Adding XSLT Extensions")) - { + { xslArgs = AddXsltExtensions(); var lib = new library(); xslArgs.AddExtensionObject("urn:umbraco.library", lib); } - + // Add parameters if (parameters == null || !parameters.ContainsKey("currentPage")) { @@ -1067,8 +1078,8 @@ namespace umbraco using (DisposableTimer.DebugDuration("Executing XSLT transform")) { xslt.Transform(macroXml.CreateNavigator(), xslArgs, tw); - } - return TemplateUtilities.ResolveUrlsFromTextString(tw.ToString()); + } + return TemplateUtilities.ResolveUrlsFromTextString(tw.ToString()); } // gets the result of the xslt transform with no parameters - Navigator mode @@ -1128,7 +1139,7 @@ namespace umbraco return XsltExtensionsResolver.Current.XsltExtensions .ToDictionary(x => x.Namespace, x => x.ExtensionObject); } - + /// /// Returns an XSLT argument list with all XSLT extensions added, /// both predefined and configured ones. @@ -1142,9 +1153,9 @@ namespace umbraco { string extensionNamespace = "urn:" + extension.Key; xslArgs.AddExtensionObject(extensionNamespace, extension.Value); - TraceInfo("umbracoXsltExtension", - String.Format("Extension added: {0}, {1}", - extensionNamespace, extension.Value.GetType().Name)); + TraceInfo("umbracoXsltExtension", + String.Format("Extension added: {0}, {1}", + extensionNamespace, extension.Value.GetType().Name)); } return xslArgs; @@ -1162,12 +1173,12 @@ namespace umbraco // if no value is passed, then use the current "pageID" as value var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; - TraceInfo("umbracoMacro", - "Xslt node adding search start (" + macroPropertyAlias + ",'" + - macroPropertyValue + "')"); - + TraceInfo("umbracoMacro", + "Xslt node adding search start (" + macroPropertyAlias + ",'" + + macroPropertyValue + "')"); + //TODO: WE need to fix this so that we give control of this stuff over to the actual parameter editors! - + switch (macroPropertyType) { case "contentTree": @@ -1197,7 +1208,7 @@ namespace umbraco macroXmlNode.AppendChild(currentNode); - break; + break; case "contentAll": macroXmlNode.AppendChild(macroXml.ImportNode(umbracoXml.DocumentElement, true)); @@ -1205,33 +1216,33 @@ namespace umbraco case "contentRandom": XmlNode source = umbracoXml.GetElementById(contentId); - if (source != null) - { - var sourceList = source.SelectNodes("node|*[@isDoc]"); - if (sourceList.Count > 0) - { - int rndNumber; - var r = library.GetRandom(); - lock (r) - { - rndNumber = r.Next(sourceList.Count); - } - var node = macroXml.ImportNode(sourceList[rndNumber], true); - // remove all sub content nodes - foreach (XmlNode n in node.SelectNodes("node|*[@isDoc]")) - node.RemoveChild(n); + if (source != null) + { + var sourceList = source.SelectNodes("node|*[@isDoc]"); + if (sourceList.Count > 0) + { + int rndNumber; + var r = library.GetRandom(); + lock (r) + { + rndNumber = r.Next(sourceList.Count); + } + var node = macroXml.ImportNode(sourceList[rndNumber], true); + // remove all sub content nodes + foreach (XmlNode n in node.SelectNodes("node|*[@isDoc]")) + node.RemoveChild(n); - macroXmlNode.AppendChild(node); - } - else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't have children!"); - } - else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't exists!"); + macroXmlNode.AppendChild(node); + } + else + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't have children!"); + } + else + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't exists!"); break; case "mediaCurrent": @@ -1252,14 +1263,14 @@ namespace umbraco // add parameters to the macro parameters collection private void AddMacroParameter(ICollection parameters, NavigableNavigator contentNavigator, NavigableNavigator mediaNavigator, - string macroPropertyAlias,string macroPropertyType, string macroPropertyValue) + string macroPropertyAlias, string macroPropertyType, string macroPropertyValue) { // if no value is passed, then use the current "pageID" as value var contentId = macroPropertyValue == string.Empty ? UmbracoContext.Current.PageId.ToString() : macroPropertyValue; - TraceInfo("umbracoMacro", - "Xslt node adding search start (" + macroPropertyAlias + ",'" + - macroPropertyValue + "')"); + TraceInfo("umbracoMacro", + "Xslt node adding search start (" + macroPropertyAlias + ",'" + + macroPropertyValue + "')"); // beware! do not use the raw content- or media- navigators, but clones !! @@ -1315,15 +1326,15 @@ namespace umbraco if (node != null) { nav = contentNavigator.CloneWithNewRoot(node.Id.ToString(CultureInfo.InvariantCulture)); - parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, nav, 0)); + parameters.Add(new MacroNavigator.MacroParameter(macroPropertyAlias, nav, 0)); } else throw new InvalidOperationException("Iterator contains non-INavigableContent elements."); } else - TraceWarn("umbracoMacro", - "Error adding random node - parent (" + macroPropertyValue + - ") doesn't have children!"); + TraceWarn("umbracoMacro", + "Error adding random node - parent (" + macroPropertyValue + + ") doesn't have children!"); } else TraceWarn("umbracoMacro", @@ -1346,31 +1357,31 @@ namespace umbraco #endregion - /// - /// Renders a Partial View Macro - /// - /// - /// - internal ScriptingMacroResult LoadPartialViewMacro(MacroModel macro) - { - var retVal = new ScriptingMacroResult(); - IMacroEngine engine = null; - - engine = MacroEngineFactory.GetEngine(PartialViewMacroEngine.EngineName); - var ret = engine.Execute(macro, GetCurrentNode()); + /// + /// Renders a Partial View Macro + /// + /// + /// + internal ScriptingMacroResult LoadPartialViewMacro(MacroModel macro) + { + var retVal = new ScriptingMacroResult(); + IMacroEngine engine = null; - // if the macro engine supports success reporting and executing failed, then return an empty control so it's not cached - if (engine is IMacroEngineResultStatus) - { - var result = engine as IMacroEngineResultStatus; - if (!result.Success) - { - retVal.ResultException = result.ResultException; - } - } - retVal.Result = ret; - return retVal; - } + engine = MacroEngineFactory.GetEngine(PartialViewMacroEngine.EngineName); + var ret = engine.Execute(macro, GetCurrentNode()); + + // if the macro engine supports success reporting and executing failed, then return an empty control so it's not cached + if (engine is IMacroEngineResultStatus) + { + var result = engine as IMacroEngineResultStatus; + if (!result.Success) + { + retVal.ResultException = result.ResultException; + } + } + retVal.Result = ret; + return retVal; + } public ScriptingMacroResult loadMacroScript(MacroModel macro) { @@ -1403,7 +1414,7 @@ namespace umbraco retVal.Result = ret; return retVal; } - + /// /// Loads a custom or webcontrol using reflection into the macro object /// @@ -1432,8 +1443,8 @@ namespace umbraco if (!File.Exists(currentAss)) return new LiteralControl("Unable to load user control because is does not exist: " + fileName); asm = Assembly.LoadFrom(currentAss); - - TraceInfo("umbracoMacro", "Assembly file " + currentAss + " LOADED!!"); + + TraceInfo("umbracoMacro", "Assembly file " + currentAss + " LOADED!!"); } catch { @@ -1442,7 +1453,7 @@ namespace umbraco ".dll"))); } - TraceInfo("umbracoMacro", string.Format("Assembly Loaded from ({0}.dll)", fileName)); + TraceInfo("umbracoMacro", string.Format("Assembly Loaded from ({0}.dll)", fileName)); type = asm.GetType(controlName); if (type == null) return new LiteralControl(string.Format("Unable to get type {0} from assembly {1}", @@ -1470,8 +1481,8 @@ namespace umbraco { var prop = type.GetProperty(mp.Key); if (prop == null) - { - TraceWarn("macro", string.Format("control property '{0}' doesn't exist or aren't accessible (public)", mp.Key)); + { + TraceWarn("macro", string.Format("control property '{0}' doesn't exist or aren't accessible (public)", mp.Key)); continue; } @@ -1507,17 +1518,17 @@ namespace umbraco } } - /// - /// Loads an usercontrol using reflection into the macro object - /// - /// Filename of the usercontrol - ie. ~wulff.ascx - /// - /// The page elements. - /// - public Control loadUserControl(string fileName, MacroModel model, Hashtable pageElements) + /// + /// Loads an usercontrol using reflection into the macro object + /// + /// Filename of the usercontrol - ie. ~wulff.ascx + /// + /// The page elements. + /// + public Control loadUserControl(string fileName, MacroModel model, Hashtable pageElements) { - Mandate.ParameterNotNullOrEmpty(fileName, "fileName"); - Mandate.ParameterNotNull(model, "model"); + Mandate.ParameterNotNullOrEmpty(fileName, "fileName"); + Mandate.ParameterNotNull(model, "model"); try { @@ -1541,13 +1552,13 @@ namespace umbraco TraceInfo(LoadUserControlKey, string.Format("Usercontrol added with id '{0}'", oControl.ID)); - AddCurrentNodeToControl(oControl); + AddCurrentNodeToControl(oControl); UpdateControlProperties(oControl, model); return oControl; } catch (Exception e) { - LogHelper.WarnWithException(string.Format("Error creating usercontrol ({0})", fileName), true, e); + LogHelper.WarnWithException(string.Format("Error creating usercontrol ({0})", fileName), true, e); throw; } } @@ -1578,7 +1589,7 @@ namespace umbraco //Trace out to profiling... doesn't actually profile, just for informational output. if (excludeProfiling == false) { - using (ApplicationContext.Current.ProfilingLogger.TraceDuration(string.Format("{0}", message))) + using (ApplicationContext.Current.ProfilingLogger.DebugDuration(string.Format("{0}", message))) { } } @@ -1587,7 +1598,7 @@ namespace umbraco private static void TraceWarn(string category, string message, bool excludeProfiling = false) { if (HttpContext.Current != null) - HttpContext.Current.Trace.Warn(category, message); + HttpContext.Current.Trace.Warn(category, message); //Trace out to profiling... doesn't actually profile, just for informational output. if (excludeProfiling == false) @@ -1599,9 +1610,9 @@ namespace umbraco } private static void TraceWarn(string category, string message, Exception ex, bool excludeProfiling = false) - { - if (HttpContext.Current != null) - HttpContext.Current.Trace.Warn(category, message, ex); + { + if (HttpContext.Current != null) + HttpContext.Current.Trace.Warn(category, message, ex); //Trace out to profiling... doesn't actually profile, just for informational output. if (excludeProfiling == false) @@ -1610,7 +1621,7 @@ namespace umbraco { } } - } + } public static string renderMacroStartTag(Hashtable attributes, int pageId, Guid versionId) { @@ -1678,7 +1689,7 @@ namespace umbraco { return "Cannot render macro content in the rich text editor when the application is running in a Partial Trust environment"; } - + string tempAlias = (attributes["macroalias"] != null) ? attributes["macroalias"].ToString() : attributes["macroAlias"].ToString(); @@ -1705,6 +1716,10 @@ namespace umbraco // propagate the user's context // zb-00004 #29956 : refactor cookies names & handling + + //TODO: This is the worst thing ever. This will also not work if people decide to put their own + // custom auth system in place. + HttpCookie inCookie = StateHelper.Cookies.UserContext.RequestCookie; var cookie = new Cookie(inCookie.Name, inCookie.Value, inCookie.Path, HttpContext.Current.Request.ServerVariables["SERVER_NAME"]); @@ -1864,7 +1879,7 @@ namespace umbraco var pageId = UmbracoContext.Current.PageId; content = pageId.HasValue ? UmbracoContext.Current.ContentCache.GetById(pageId.Value) : null; } - + return content == null ? null : LegacyNodeHelper.ConvertToNode(content); } diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index ce2b55152b..e71a290754 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -70,7 +70,7 @@ namespace umbraco var docParentId = -1; try { - docParentId = document.Parent.Id; + docParentId = document.ParentId; } catch (ArgumentException) { diff --git a/src/Umbraco.Web/umbraco.presentation/template.cs b/src/Umbraco.Web/umbraco.presentation/template.cs index 6c1e23a13e..dfdad324e2 100644 --- a/src/Umbraco.Web/umbraco.presentation/template.cs +++ b/src/Umbraco.Web/umbraco.presentation/template.cs @@ -494,7 +494,7 @@ namespace umbraco { var tId = templateID; - var t = ApplicationContext.Current.ApplicationCache.GetCacheItem( + var t = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem