diff --git a/.github/README.md b/.github/README.md index 5a1340006e..ba5293e8c2 100644 --- a/.github/README.md +++ b/.github/README.md @@ -6,7 +6,7 @@ _Ready to try out Version 8? [See the quick start guide](V8_GETTING_STARTED.md). When is Umbraco 8 coming? ========================= -When it's ready. We're done with the major parts of the architecture work and are focusing on three seperate tracks to prepare Umbraco 8 for release: +When it's ready. We're done with the major parts of the architecture work and are focusing on three separate tracks to prepare Umbraco 8 for release: 1) Editor Track (_currently in progress_). Without editors, there's no market for Umbraco. So we want to make sure that Umbraco 8 is full of love for editors. 2) Partner Track. Without anyone implementing Umbraco, there's nothing for editors to update. So we want to make sure that Umbraco 8 is a joy to implement 3) Contributor Track. Without our fabulous ecosystem of both individual Umbracians and 3rd party ISVs, Umbraco wouldn't be as rich a platform as it is today. We want to make sure that it's easy, straight forward and as backwards-compatible as possible to create packages for Umbraco diff --git a/.gitignore b/.gitignore index 279bdb39dd..57b4e18aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ *.suo *.vs10x *.ndproj +*.ignorer.* # common directories .DS_Store @@ -156,4 +157,4 @@ build/temp/ -# eof \ No newline at end of file +# eof diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 92589ab6cf..7d9f43b5fe 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -20,16 +20,18 @@ namespace Umbraco.Core.Deploy /// Gets the deploy property value corresponding to a content property value, and gather dependencies. /// /// The content property value. + /// The value property type /// The content dependencies. /// The deploy property value. - string ToArtifact(object value, ICollection dependencies); + string ToArtifact(object value, PropertyType propertyType, ICollection dependencies); /// /// Gets the content property value corresponding to a deploy property value. /// /// The deploy property value. + /// The value property type< /// The current content property value. /// The content property value. - object FromArtifact(string value, object currentValue); + object FromArtifact(string value, PropertyType propertyType, object currentValue); } } diff --git a/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs index 2d333ed916..1c6f9853a2 100644 --- a/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs +++ b/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs @@ -92,7 +92,7 @@ namespace Umbraco.Core.Logging.Serilog /// /// Reads settings from /config/serilog.user.config - /// That allows a seperate logging pipeline to be configured that wil not affect the main Umbraco log + /// That allows a separate logging pipeline to be configured that wil not affect the main Umbraco log /// /// A Serilog LoggerConfiguration public static LoggerConfiguration ReadFromUserConfigFile(this LoggerConfiguration logConfig) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 51935e6517..833955ee6a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -3,6 +3,7 @@ using System.Configuration; using Semver; using Umbraco.Core.Configuration; using Umbraco.Core.Migrations.Upgrade.V_7_12_0; +using Umbraco.Core.Migrations.Upgrade.V_7_14_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_0; namespace Umbraco.Core.Migrations.Upgrade @@ -122,6 +123,7 @@ namespace Umbraco.Core.Migrations.Upgrade To("{C39BF2A7-1454-4047-BBFE-89E40F66ED63}"); To("{64EBCE53-E1F0-463A-B40B-E98EFCCA8AE2}"); To("{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}"); + To("{8A027815-D5CD-4872-8B88-9A51AB5986A6}"); // from 7.14.0 //FINAL @@ -145,12 +147,23 @@ namespace Umbraco.Core.Migrations.Upgrade // main chain, skipping the migrations // From("{init-7.12.0}"); - // start stop target + // clone start / clone stop / target ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); From("{init-7.12.1}").To("{init-7.10.0}"); // same as 7.12.0 From("{init-7.12.2}").To("{init-7.10.0}"); // same as 7.12.0 From("{init-7.12.3}").To("{init-7.10.0}"); // same as 7.12.0 + From("{init-7.12.4}").To("{init-7.10.0}"); // same as 7.12.0 + From("{init-7.13.0}").To("{init-7.10.0}"); // same as 7.12.0 + From("{init-7.13.1}").To("{init-7.10.0}"); // same as 7.12.0 + + // 7.14.0 has migrations, handle it... + // clone going from 7.10 to 1350617A (the last one before we started to merge 7.12 migrations), then + // clone going from CF51B39B (after 7.12 migrations) to 0009109C (the last one before we started to merge 7.12 migrations), + // ending in 8A027815 (after 7.14 migrations) + From("{init-7.14.0}") + .ToWithClone("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{9109B8AF-6B34-46EE-9484-7434196D0C79}") + .ToWithClone("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}", "{0009109C-A0B8-4F3F-8FEB-C137BBDDA268}", "{8A027815-D5CD-4872-8B88-9A51AB5986A6}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_14_0/UpdateMemberGroupPickerData.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_14_0/UpdateMemberGroupPickerData.cs new file mode 100644 index 0000000000..c70f42076f --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_14_0/UpdateMemberGroupPickerData.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Core.Migrations.Upgrade.V_7_14_0 +{ + /// + /// Migrates member group picker properties from NVarchar to NText. See https://github.com/umbraco/Umbraco-CMS/issues/3268. + /// + public class UpdateMemberGroupPickerData : MigrationBase + { + /// + /// Migrates member group picker properties from NVarchar to NText. See https://github.com/umbraco/Umbraco-CMS/issues/3268. + /// + public UpdateMemberGroupPickerData(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + Database.Execute($@"UPDATE umbracoPropertyData SET textValue = varcharValue, varcharValue = NULL + WHERE textValue IS NULL AND id IN ( + SELECT id FROM umbracoPropertyData WHERE propertyTypeId in ( + SELECT id from cmsPropertyType where dataTypeId IN ( + SELECT nodeId FROM umbracoDataType WHERE propertyEditorAlias = '{Constants.PropertyEditors.Aliases.MemberGroupPicker}' + ) + ) + )"); + + Database.Execute($"UPDATE umbracoDataType SET dbType = 'Ntext' WHERE propertyEditorAlias = '{Constants.PropertyEditors.Aliases.MemberGroupPicker}'"); + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs index ee3fd62985..8be56850d1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedModelFactory.cs @@ -68,7 +68,7 @@ namespace Umbraco.Core.Models.PublishedContent var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; if (modelInfos.TryGetValue(typeName, out var modelInfo)) - throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); + throw new InvalidOperationException($"Both types '{type.AssemblyQualifiedName}' and '{modelInfo.ModelType.AssemblyQualifiedName}' want to be a model type for content type with alias \"{typeName}\"."); // have to use an unsafe ctor because we don't know the types, really var modelCtor = ReflectionUtilities.EmitConstructorUnsafe>(constructor); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 90fc472e30..b6a1103097 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -892,7 +892,7 @@ namespace Umbraco.Core } /// - /// Ensures that the folder path ends with a DirectorySeperatorChar + /// Ensures that the folder path ends with a DirectorySeparatorChar /// /// /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 41f2bc4951..d72c0e5014 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -368,6 +368,7 @@ + diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index 3af930c4c3..73f3cd1537 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -97,7 +97,7 @@ namespace Umbraco.Tests.PublishedContent { var doc = GetContent(true, 1); //change a doc type alias - var c = (TestPublishedContent) doc.Children.ElementAt(0); + var c = (TestPublishedContent)doc.Children.ElementAt(0); c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var dt = doc.ChildrenAsTable(Current.Services, "Child"); diff --git a/src/Umbraco.Tests/Services/Importing/Fanoe-Package.xml b/src/Umbraco.Tests/Services/Importing/Fanoe-Package.xml index 5f6371bba1..baadf0340f 100644 --- a/src/Umbraco.Tests/Services/Importing/Fanoe-Package.xml +++ b/src/Umbraco.Tests/Services/Importing/Fanoe-Package.xml @@ -4163,7 +4163,7 @@ html { text-align: center; } -.text--center .seperator { +.text--center .separator { margin-right: auto; margin-left: auto; } @@ -4998,4 +4998,4 @@ nav > ul li.active ul li a { - \ No newline at end of file + diff --git a/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs b/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs index 3c7377f2cc..134b709447 100644 --- a/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs +++ b/src/Umbraco.Tests/Testing/Objects/Accessors/TestVariationContextAccessor.cs @@ -8,10 +8,6 @@ namespace Umbraco.Tests.Testing.Objects.Accessors public class TestVariationContextAccessor : IVariationContextAccessor { /// - public VariationContext VariationContext - { - get; - set; - } + public VariationContext VariationContext { get; set; } } } diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js new file mode 100755 index 0000000000..c27a2c5f53 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = { + sources: { + + //less files used by backoffice and preview + //processed in the less task + less: { + installer: { files: ["./src/less/installer.less"], out: "installer.css" }, + nonodes: { files: ["./src/less/pages/nonodes.less"], out: "nonodes.style.min.css"}, + preview: { files: ["./src/less/canvas-designer.less"], out: "canvasdesigner.css" }, + umbraco: { files: ["./src/less/belle.less"], out: "umbraco.css" } + }, + + //js files for backoffie + //processed in the js task + js: { + preview: { files: ["./src/preview/**/*.js"], out: "umbraco.preview.js" }, + installer: { files: ["./src/installer/**/*.js"], out: "umbraco.installer.js" }, + controllers: { files: ["./src/{views,controllers}/**/*.controller.js"], out: "umbraco.controllers.js" }, + directives: { files: ["./src/common/directives/**/*.js"], out: "umbraco.directives.js" }, + filters: { files: ["./src/common/filters/**/*.js"], out: "umbraco.filters.js" }, + resources: { files: ["./src/common/resources/**/*.js"], out: "umbraco.resources.js" }, + services: { files: ["./src/common/services/**/*.js"], out: "umbraco.services.js" }, + security: { files: ["./src/common/interceptors/**/*.js"], out: "umbraco.interceptors.js" } + }, + + //selectors for copying all views into the build + //processed in the views task + views:{ + umbraco: {files: ["./src/views/**/*.html"], folder: ""}, + installer: {files: ["./src/installer/steps/*.html"], folder: "install/"} + }, + + //globs for file-watching + globs:{ + views: "./src/views/**/*.html", + less: "./src/less/**/*.less", + js: "./src/*.js", + lib: "./lib/**/*", + assets: "./src/assets/**" + } + }, + root: "../Umbraco.Web.UI/Umbraco/", + targets: { + js: "js/", + lib: "lib/", + views: "views/", + css: "assets/css/", + assets: "assets/" + } +}; diff --git a/src/Umbraco.Web.UI.Client/gulp/index.js b/src/Umbraco.Web.UI.Client/gulp/index.js new file mode 100755 index 0000000000..9dd80598af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/index.js @@ -0,0 +1,10 @@ +'use strict'; + +var fs = require('fs'); + +var onlyScripts = require('./util/scriptFilter'); +var tasks = fs.readdirSync('./gulp/tasks/').filter(onlyScripts); + +tasks.forEach(function(task) { + require('./tasks/' + task); +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/build.js b/src/Umbraco.Web.UI.Client/gulp/tasks/build.js new file mode 100644 index 0000000000..4e519c58e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/build.js @@ -0,0 +1,10 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); +var runSequence = require('run-sequence'); + +// Build - build the files ready for production +gulp.task('build', function(cb) { + runSequence(["dependencies", "js", "less", "views"], cb); +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js new file mode 100644 index 0000000000..8032274ee7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js @@ -0,0 +1,288 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var MergeStream = require('merge-stream'); + +var imagemin = require('gulp-imagemin'); + +/************************** + * Task processes and copies all dependencies, either installed by npm or stored locally in the project + **************************/ +gulp.task('dependencies', function () { + + //as we do multiple things in this task, we merge the multiple streams + var stream = new MergeStream(); + + // Pick the dependencies we need from each package + // so we don't just ship with a lot of files that aren't needed + const nodeModules = [ + { + "name": "ace-builds", + "src": [ + "./node_modules/ace-builds/src-min-noconflict/ace.js", + "./node_modules/ace-builds/src-min-noconflict/ext-language_tools.js", + "./node_modules/ace-builds/src-min-noconflict/ext-searchbox.js", + "./node_modules/ace-builds/src-min-noconflict/ext-settings_menu.js", + "./node_modules/ace-builds/src-min-noconflict/snippets/text.js", + "./node_modules/ace-builds/src-min-noconflict/snippets/javascript.js", + "./node_modules/ace-builds/src-min-noconflict/snippets/css.js", + "./node_modules/ace-builds/src-min-noconflict/theme-chrome.js", + "./node_modules/ace-builds/src-min-noconflict/mode-razor.js", + "./node_modules/ace-builds/src-min-noconflict/mode-javascript.js", + "./node_modules/ace-builds/src-min-noconflict/mode-css.js", + "./node_modules/ace-builds/src-min-noconflict/worker-javascript.js", + "./node_modules/ace-builds/src-min-noconflict/worker-css.js" + ], + "base": "./node_modules/ace-builds" + }, + { + "name": "angular", + "src": ["./node_modules/angular/angular.js"], + "base": "./node_modules/angular" + }, + { + "name": "angular-cookies", + "src": ["./node_modules/angular-cookies/angular-cookies.js"], + "base": "./node_modules/angular-cookies" + }, + { + "name": "angular-dynamic-locale", + "src": [ + "./node_modules/angular-dynamic-locale/dist/tmhDynamicLocale.min.js", + "./node_modules/angular-dynamic-locale/dist/tmhDynamicLocale.min.js.map" + ], + "base": "./node_modules/angular-dynamic-locale/dist" + }, + { + "name": "angular-sanitize", + "src": ["./node_modules/angular-sanitize/angular-sanitize.js"], + "base": "./node_modules/angular-sanitize" + }, + { + "name": "angular-touch", + "src": ["./node_modules/angular-touch/angular-touch.js"], + "base": "./node_modules/angular-touch" + }, + { + "name": "angular-ui-sortable", + "src": ["./node_modules/angular-ui-sortable/dist/sortable.js"], + "base": "./node_modules/angular-ui-sortable/dist" + }, + { + "name": "angular-route", + "src": ["./node_modules/angular-route/angular-route.js"], + "base": "./node_modules/angular-route" + }, + { + "name": "angular-animate", + "src": ["./node_modules/angular-animate/angular-animate.js"], + "base": "./node_modules/angular-animate" + }, + { + "name": "angular-i18n", + "src": [ + "./node_modules/angular-i18n/angular-i18n.js", + "./node_modules/angular-i18n/angular-locale_*.js" + ], + "base": "./node_modules/angular-i18n" + }, + { + "name": "angular-local-storage", + "src": [ + "./node_modules/angular-local-storage/dist/angular-local-storage.min.js", + "./node_modules/angular-local-storage/dist/angular-local-storage.min.js.map" + ], + "base": "./node_modules/angular-local-storage/dist" + }, + { + "name": "angular-messages", + "src": ["./node_modules/angular-messages/angular-messages.js"], + "base": "./node_modules/angular-messages" + }, + { + "name": "angular-mocks", + "src": ["./node_modules/angular-mocks/angular-mocks.js"], + "base": "./node_modules/angular-mocks" + }, + { + "name": "animejs", + "src": ["./node_modules/animejs/anime.min.js"], + "base": "./node_modules/animejs" + }, + { + "name": "bootstrap-social", + "src": ["./node_modules/bootstrap-social/bootstrap-social.css"], + "base": "./node_modules/bootstrap-social" + }, + + { + "name": "angular-chart.js", + "src": ["./node_modules/angular-chart.js/dist/angular-chart.min.js"], + "base": "./node_modules/angular-chart.js/dist" + }, + { + "name": "chart.js", + "src": ["./node_modules/chart.js/dist/chart.min.js"], + "base": "./node_modules/chart.js/dist" + }, + { + "name": "clipboard", + "src": ["./node_modules/clipboard/dist/clipboard.min.js"], + "base": "./node_modules/clipboard/dist" + }, + { + "name": "jsdiff", + "src": ["./node_modules/diff/dist/diff.min.js"], + "base": "./node_modules/diff/dist" + }, + { + "name": "flatpickr", + "src": [ + "./node_modules/flatpickr/dist/flatpickr.js", + "./node_modules/flatpickr/dist/flatpickr.css" + ], + "base": "./node_modules/flatpickr/dist" + }, + { + "name": "font-awesome", + "src": [ + "./node_modules/font-awesome/fonts/*", + "./node_modules/font-awesome/css/font-awesome.min.css" + ], + "base": "./node_modules/font-awesome" + }, + { + "name": "jquery", + "src": [ + "./node_modules/jquery/dist/jquery.min.js", + "./node_modules/jquery/dist/jquery.min.map" + ], + "base": "./node_modules/jquery/dist" + }, + { + "name": "jquery-ui", + "src": ["./node_modules/jquery-ui-dist/jquery-ui.min.js"], + "base": "./node_modules/jquery-ui-dist" + }, + { + "name": "jquery-ui-touch-punch", + "src": ["./node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"], + "base": "./node_modules/jquery-ui-touch-punch" + }, + { + "name": "lazyload-js", + "src": ["./node_modules/lazyload-js/lazyload.min.js"], + "base": "./node_modules/lazyload-js" + }, + { + "name": "moment", + "src": ["./node_modules/moment/min/moment.min.js"], + "base": "./node_modules/moment/min" + }, + { + "name": "moment", + "src": ["./node_modules/moment/locale/*.js"], + "base": "./node_modules/moment/locale" + }, + { + "name": "ng-file-upload", + "src": ["./node_modules/ng-file-upload/dist/ng-file-upload.min.js"], + "base": "./node_modules/ng-file-upload/dist" + }, + { + "name": "nouislider", + "src": [ + "./node_modules/nouislider/distribute/nouislider.min.js", + "./node_modules/nouislider/distribute/nouislider.min.css" + ], + "base": "./node_modules/nouislider/distribute" + }, + { + "name": "signalr", + "src": ["./node_modules/signalr/jquery.signalR.js"], + "base": "./node_modules/signalr" + }, + { + "name": "spectrum", + "src": [ + "./node_modules/spectrum-colorpicker/spectrum.js", + "./node_modules/spectrum-colorpicker/spectrum.css" + ], + "base": "./node_modules/spectrum-colorpicker" + }, + { + "name": "tinymce", + "src": [ + "./node_modules/tinymce/tinymce.min.js", + "./node_modules/tinymce/plugins/**", + "./node_modules/tinymce/skins/**", + "./node_modules/tinymce/themes/**" + ], + "base": "./node_modules/tinymce" + }, + { + "name": "typeahead.js", + "src": ["./node_modules/typeahead.js/dist/typeahead.bundle.min.js"], + "base": "./node_modules/typeahead.js/dist" + }, + { + "name": "underscore", + "src": ["node_modules/underscore/underscore-min.js"], + "base": "./node_modules/underscore" + } + ]; + + // add streams for node modules + nodeModules.forEach(module => { + stream.add( + gulp.src(module.src, + { base: module.base }) + .pipe(gulp.dest(config.root + config.targets.lib + "/" + module.name)) + ); + }); + + //copy over libs which are not on npm (/lib) + stream.add( + gulp.src(config.sources.globs.lib) + .pipe(gulp.dest(config.root + config.targets.lib)) + ); + + //Copies all static assets into /root / assets folder + //css, fonts and image files + stream.add( + gulp.src(config.sources.globs.assets) + .pipe(imagemin([ + imagemin.gifsicle({interlaced: true}), + imagemin.jpegtran({progressive: true}), + imagemin.optipng({optimizationLevel: 5}), + imagemin.svgo({ + plugins: [ + {removeViewBox: true}, + {cleanupIDs: false} + ] + }) + ])) + .pipe(gulp.dest(config.root + config.targets.assets)) + ); + + // Copies all the less files related to the preview into their folder + //these are not pre-processed as preview has its own less combiler client side + stream.add( + gulp.src("src/canvasdesigner/editors/*.less") + .pipe(gulp.dest(config.root + config.targets.assets + "/less")) + ); + + // Todo: check if we need these fileSize + stream.add( + gulp.src("src/views/propertyeditors/grid/config/*.*") + .pipe(gulp.dest(config.root + config.targets.views + "/propertyeditors/grid/config")) + ); + stream.add( + gulp.src("src/views/dashboard/default/*.jpg") + .pipe(gulp.dest(config.root + config.targets.views + "/dashboard/default")) + ); + + return stream; +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js new file mode 100644 index 0000000000..bca4da8c43 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dev.js @@ -0,0 +1,10 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); +var runSequence = require('run-sequence'); + +// Dev - build the files ready for development and start watchers +gulp.task('dev', function(cb) { + runSequence(["dependencies", "js", "less", "views"], "watch", cb); +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/docs.js b/src/Umbraco.Web.UI.Client/gulp/tasks/docs.js new file mode 100644 index 0000000000..34271a018c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/docs.js @@ -0,0 +1,55 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var connect = require('gulp-connect'); +var open = require('gulp-open'); +var gulpDocs = require('gulp-ngdocs'); + +/************************** + * Build Backoffice UI API documentation + **************************/ +gulp.task('docs', [], function (cb) { + + var options = { + html5Mode: false, + startPage: '/api', + title: "Umbraco Backoffice UI API Documentation", + dest: 'docs/api', + styles: ['docs/umb-docs.css'], + image: "https://our.umbraco.com/assets/images/logo.svg" + } + + return gulpDocs.sections({ + api: { + glob: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], + api: true, + title: 'API Documentation' + } + }) + .pipe(gulpDocs.process(options)) + .pipe(gulp.dest('docs/api')); + cb(); +}); + +gulp.task('connect:docs', function (cb) { + connect.server({ + root: 'docs/api', + livereload: true, + fallback: 'docs/api/index.html', + port: 8880 + }); + cb(); +}); + +gulp.task('open:docs', function (cb) { + + var options = { + uri: 'http://localhost:8880/index.html' + }; + + gulp.src(__filename) + .pipe(open(options)); + cb(); +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/docserve.js b/src/Umbraco.Web.UI.Client/gulp/tasks/docserve.js new file mode 100644 index 0000000000..a402003383 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/docserve.js @@ -0,0 +1,10 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); +var runSequence = require('run-sequence'); + +// Docserve - build and open the back office documentation +gulp.task('docserve', function(cb) { + runSequence('docs', 'connect:docs', 'open:docs', cb); +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/js.js b/src/Umbraco.Web.UI.Client/gulp/tasks/js.js new file mode 100644 index 0000000000..e3ea0cc9d8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/js.js @@ -0,0 +1,29 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); + +var processJs = require('../util/processJs'); + +/************************** + * Copies all angular JS files into their seperate umbraco.*.js file + **************************/ +gulp.task('js', function () { + + //we run multiple streams, so merge them all together + var stream = new MergeStream(); + + stream.add( + gulp.src(config.sources.globs.js) + .pipe(gulp.dest(config.root + config.targets.js)) + ); + + _.forEach(config.sources.js, function (group) { + stream.add (processJs(group.files, group.out) ); + }); + + return stream; +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/less.js b/src/Umbraco.Web.UI.Client/gulp/tasks/less.js new file mode 100644 index 0000000000..3d19716b67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/less.js @@ -0,0 +1,20 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); + +var processLess = require('../util/processLess'); + +gulp.task('less', function () { + + var stream = new MergeStream(); + + _.forEach(config.sources.less, function (group) { + stream.add( processLess(group.files, group.out) ); + }); + + return stream; +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js new file mode 100644 index 0000000000..e40d4c436a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js @@ -0,0 +1,26 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); +var karmaServer = require('karma').Server; + +/************************** + * Build tests + **************************/ + + // Karma test +gulp.task('test:unit', function() { + new karmaServer({ + configFile: __dirname + "/test/config/karma.conf.js", + keepalive: true + }) + .start(); +}); + +gulp.task('test:e2e', function() { + new karmaServer({ + configFile: __dirname + "/test/config/e2e.js", + keepalive: true + }) + .start(); +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/views.js b/src/Umbraco.Web.UI.Client/gulp/tasks/views.js new file mode 100644 index 0000000000..05246370c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/views.js @@ -0,0 +1,25 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); + +gulp.task('views', function () { + + var stream = new MergeStream(); + + _.forEach(config.sources.views, function (group) { + + console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder) + + stream.add ( + gulp.src(group.files) + .pipe( gulp.dest(config.root + config.targets.views + group.folder) ) + ); + + }); + + return stream; +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watch.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watch.js new file mode 100644 index 0000000000..a5c476f7d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watch.js @@ -0,0 +1,58 @@ +'use strict'; + +var config = require('../config'); +var gulp = require('gulp'); + +var _ = require('lodash'); +var MergeStream = require('merge-stream'); + +var processJs = require('../util/processJs'); + +var watch = require('gulp-watch'); + +gulp.task('watch', function () { + + var stream = new MergeStream(); + var watchInterval = 500; + + //Setup a watcher for all groups of javascript files + _.forEach(config.sources.js, function (group) { + + if(group.watch !== false){ + + stream.add( + + watch(group.files, { ignoreInitial: true, interval: watchInterval }, function (file) { + + console.info(file.path + " has changed, added to: " + group.out); + processJs(group.files, group.out); + + }) + + ); + + } + + }); + + stream.add( + //watch all less files and trigger the less task + watch(config.sources.globs.less, { ignoreInitial: true, interval: watchInterval }, function () { + gulp.run(['less']); + }) + ); + + //watch all views - copy single file changes + stream.add( + watch(config.sources.globs.views, { interval: watchInterval }) + .pipe(gulp.dest(config.root + config.targets.views)) + ); + + //watch all app js files that will not be merged - copy single file changes + stream.add( + watch(config.sources.globs.js, { interval: watchInterval }) + .pipe(gulp.dest(config.root + config.targets.js)) + ); + + return stream; +}); diff --git a/src/Umbraco.Web.UI.Client/gulp/util/handleErrors.js b/src/Umbraco.Web.UI.Client/gulp/util/handleErrors.js new file mode 100755 index 0000000000..3c32a928dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/util/handleErrors.js @@ -0,0 +1,18 @@ +'use strict'; + +var notify = require('gulp-notify'); + +module.exports = function(error) { + + var args = Array.prototype.slice.call(arguments); + + // Send error to notification center with gulp-notify + notify.onError({ + title: 'Compile Error', + message: '<%= error.message %>' + }).apply(this, args); + + // Keep gulp from hanging on this task + this.emit('end'); + +}; diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js new file mode 100644 index 0000000000..c110fa9cae --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js @@ -0,0 +1,26 @@ + +var config = require('../config'); +var gulp = require('gulp'); + +var eslint = require('gulp-eslint'); +var babel = require("gulp-babel"); +var sort = require('gulp-sort'); +var concat = require('gulp-concat'); +var wrap = require("gulp-wrap-js"); + +module.exports = function(files, out) { + + return gulp.src(files) + // check for js errors + .pipe(eslint()) + // outputs the lint results to the console + .pipe(eslint.format()) + // sort files in stream by path or any custom sort comparator + .pipe(babel()) + .pipe(sort()) + .pipe(concat(out)) + .pipe(wrap('(function(){\n%= body %\n})();')) + .pipe(gulp.dest(config.root + config.targets.js)); + + console.log(out + " compiled"); +}; diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processLess.js b/src/Umbraco.Web.UI.Client/gulp/util/processLess.js new file mode 100644 index 0000000000..e2bb758499 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/util/processLess.js @@ -0,0 +1,26 @@ + +var config = require('../config'); +var gulp = require('gulp'); + +var postcss = require('gulp-postcss'); +var less = require('gulp-less'); +var autoprefixer = require('autoprefixer'); +var cssnano = require('cssnano'); +var cleanCss = require("gulp-clean-css"); +var rename = require('gulp-rename'); + +module.exports = function(files, out) { + var processors = [ + autoprefixer, + cssnano({zindex: false}) + ]; + + return gulp.src(files) + .pipe(less()) + .pipe(cleanCss()) + .pipe(postcss(processors)) + .pipe(rename(out)) + .pipe(gulp.dest(config.root + config.targets.css)); + + console.log(out + " compiled"); +} diff --git a/src/Umbraco.Web.UI.Client/gulp/util/scriptFilter.js b/src/Umbraco.Web.UI.Client/gulp/util/scriptFilter.js new file mode 100755 index 0000000000..7d8ce45f82 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/gulp/util/scriptFilter.js @@ -0,0 +1,9 @@ +'use strict'; + +var path = require('path'); + +// Filters out non .js files. Prevents +// accidental inclusion of possible hidden files +module.exports = function(name) { + return /(\.(js)$)/i.test(path.extname(name)); +}; diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js old mode 100644 new mode 100755 index 22ac11b991..f01d992013 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -1,587 +1,14 @@ -var gulp = require('gulp'); -var watch = require('gulp-watch'); -var concat = require('gulp-concat'); -var rename = require('gulp-rename'); -var wrap = require("gulp-wrap-js"); -var sort = require('gulp-sort'); -var connect = require('gulp-connect'); -var open = require('gulp-open'); -var babel = require("gulp-babel"); -var runSequence = require('run-sequence'); -var imagemin = require('gulp-imagemin'); - -var _ = require('lodash'); -var MergeStream = require('merge-stream'); - -// js -var eslint = require('gulp-eslint'); - -//Less + css -var postcss = require('gulp-postcss'); -var less = require('gulp-less'); -var autoprefixer = require('autoprefixer'); -var cssnano = require('cssnano'); -var cleanCss = require("gulp-clean-css"); - -// Documentation -var gulpDocs = require('gulp-ngdocs'); - -// Testing -var karmaServer = require('karma').Server; - -/*************************************************************** -Helper functions -***************************************************************/ -function processJs(files, out) { - - return gulp.src(files) - // check for js errors - .pipe(eslint()) - // outputs the lint results to the console - .pipe(eslint.format()) - // sort files in stream by path or any custom sort comparator - .pipe(babel()) - .pipe(sort()) - .pipe(concat(out)) - .pipe(wrap('(function(){\n%= body %\n})();')) - .pipe(gulp.dest(root + targets.js)); - - console.log(out + " compiled"); -} - -function processLess(files, out) { - var processors = [ - autoprefixer, - cssnano({zindex: false}) - ]; - - return gulp.src(files) - .pipe(less()) - .pipe(cleanCss()) - .pipe(postcss(processors)) - .pipe(rename(out)) - .pipe(gulp.dest(root + targets.css)); - - console.log(out + " compiled"); -} - -/*************************************************************** -Paths and destinations -Each group is iterated automatically in the setup tasks below -***************************************************************/ -var sources = { - - //less files used by backoffice and preview - //processed in the less task - less: { - installer: { files: ["src/less/installer.less"], out: "installer.css" }, - nonodes: { files: ["src/less/pages/nonodes.less"], out: "nonodes.style.min.css"}, - preview: { files: ["src/less/canvas-designer.less"], out: "canvasdesigner.css" }, - umbraco: { files: ["src/less/belle.less"], out: "umbraco.css" } - }, - - //js files for backoffie - //processed in the js task - js: { - preview: { files: ["src/preview/**/*.js"], out: "umbraco.preview.js" }, - installer: { files: ["src/installer/**/*.js"], out: "umbraco.installer.js" }, - controllers: { files: ["src/{views,controllers}/**/*.controller.js"], out: "umbraco.controllers.js" }, - directives: { files: ["src/common/directives/**/*.js"], out: "umbraco.directives.js" }, - filters: { files: ["src/common/filters/**/*.js"], out: "umbraco.filters.js" }, - resources: { files: ["src/common/resources/**/*.js"], out: "umbraco.resources.js" }, - services: { files: ["src/common/services/**/*.js"], out: "umbraco.services.js" }, - security: { files: ["src/common/interceptors/**/*.js"], out: "umbraco.interceptors.js" } - }, - - //selectors for copying all views into the build - //processed in the views task - views:{ - umbraco: {files: ["src/views/**/*.html"], folder: ""}, - installer: {files: ["src/installer/steps/*.html"], folder: "install"} - }, - - //globs for file-watching - globs:{ - views: "./src/views/**/*.html", - less: "./src/less/**/*.less", - js: "./src/*.js", - lib: "./lib/**/*", - assets: "./src/assets/**" - } -}; - -var root = "../Umbraco.Web.UI/Umbraco/"; -var targets = { - js: "js/", - lib: "lib/", - views: "views/", - css: "assets/css/", - assets: "assets/" -}; - - -/************************** - * Main tasks for the project to prepare backoffice files - **************************/ - - // Build - build the files ready for production -gulp.task('build', function(cb) { - runSequence(["dependencies", "js", "less", "views"], cb); -}); - -// Dev - build the files ready for development and start watchers -gulp.task('dev', function(cb) { - runSequence(["dependencies", "js", "less", "views"], "watch", cb); -}); - -// Docserve - build and open the back office documentation -gulp.task('docserve', function(cb) { - runSequence('docs', 'connect:docs', 'open:docs', cb); -}); - -/************************** - * Task processes and copies all dependencies, either installed by npm or stored locally in the project - **************************/ -gulp.task('dependencies', function () { - - //as we do multiple things in this task, we merge the multiple streams - var stream = new MergeStream(); - - // Pick the dependencies we need from each package - // so we don't just ship with a lot of files that aren't needed - const nodeModules = [ - { - "name": "ace-builds", - "src": [ - "./node_modules/ace-builds/src-min-noconflict/ace.js", - "./node_modules/ace-builds/src-min-noconflict/ext-language_tools.js", - "./node_modules/ace-builds/src-min-noconflict/ext-searchbox.js", - "./node_modules/ace-builds/src-min-noconflict/ext-settings_menu.js", - "./node_modules/ace-builds/src-min-noconflict/snippets/text.js", - "./node_modules/ace-builds/src-min-noconflict/snippets/javascript.js", - "./node_modules/ace-builds/src-min-noconflict/snippets/css.js", - "./node_modules/ace-builds/src-min-noconflict/theme-chrome.js", - "./node_modules/ace-builds/src-min-noconflict/mode-razor.js", - "./node_modules/ace-builds/src-min-noconflict/mode-javascript.js", - "./node_modules/ace-builds/src-min-noconflict/mode-css.js", - "./node_modules/ace-builds/src-min-noconflict/worker-javascript.js", - "./node_modules/ace-builds/src-min-noconflict/worker-css.js" - ], - "base": "./node_modules/ace-builds" - }, - { - "name": "angular", - "src": ["./node_modules/angular/angular.js"], - "base": "./node_modules/angular" - }, - { - "name": "angular-cookies", - "src": ["./node_modules/angular-cookies/angular-cookies.js"], - "base": "./node_modules/angular-cookies" - }, - { - "name": "angular-dynamic-locale", - "src": [ - "./node_modules/angular-dynamic-locale/dist/tmhDynamicLocale.min.js", - "./node_modules/angular-dynamic-locale/dist/tmhDynamicLocale.min.js.map" - ], - "base": "./node_modules/angular-dynamic-locale/dist" - }, - { - "name": "angular-sanitize", - "src": ["./node_modules/angular-sanitize/angular-sanitize.js"], - "base": "./node_modules/angular-sanitize" - }, - { - "name": "angular-touch", - "src": ["./node_modules/angular-touch/angular-touch.js"], - "base": "./node_modules/angular-touch" - }, - { - "name": "angular-ui-sortable", - "src": ["./node_modules/angular-ui-sortable/dist/sortable.js"], - "base": "./node_modules/angular-ui-sortable/dist" - }, - { - "name": "angular-route", - "src": ["./node_modules/angular-route/angular-route.js"], - "base": "./node_modules/angular-route" - }, - { - "name": "angular-animate", - "src": ["./node_modules/angular-animate/angular-animate.js"], - "base": "./node_modules/angular-animate" - }, - { - "name": "angular-i18n", - "src": [ - "./node_modules/angular-i18n/angular-i18n.js", - "./node_modules/angular-i18n/angular-locale_*.js" - ], - "base": "./node_modules/angular-i18n" - }, - { - "name": "angular-local-storage", - "src": [ - "./node_modules/angular-local-storage/dist/angular-local-storage.min.js", - "./node_modules/angular-local-storage/dist/angular-local-storage.min.js.map" - ], - "base": "./node_modules/angular-local-storage/dist" - }, - { - "name": "angular-messages", - "src": ["./node_modules/angular-messages/angular-messages.js"], - "base": "./node_modules/angular-messages" - }, - { - "name": "angular-mocks", - "src": ["./node_modules/angular-mocks/angular-mocks.js"], - "base": "./node_modules/angular-mocks" - }, - { - "name": "animejs", - "src": ["./node_modules/animejs/anime.min.js"], - "base": "./node_modules/animejs" - }, - { - "name": "bootstrap-social", - "src": ["./node_modules/bootstrap-social/bootstrap-social.css"], - "base": "./node_modules/bootstrap-social" - }, - - { - "name": "angular-chart.js", - "src": ["./node_modules/angular-chart.js/dist/angular-chart.min.js"], - "base": "./node_modules/angular-chart.js/dist" - }, - { - "name": "chart.js", - "src": ["./node_modules/chart.js/dist/chart.min.js"], - "base": "./node_modules/chart.js/dist" - }, - { - "name": "clipboard", - "src": ["./node_modules/clipboard/dist/clipboard.min.js"], - "base": "./node_modules/clipboard/dist" - }, - { - "name": "jsdiff", - "src": ["./node_modules/diff/dist/diff.min.js"], - "base": "./node_modules/diff/dist" - }, - { - "name": "flatpickr", - "src": [ - "./node_modules/flatpickr/dist/flatpickr.js", - "./node_modules/flatpickr/dist/flatpickr.css" - ], - "base": "./node_modules/flatpickr/dist" - }, - { - "name": "font-awesome", - "src": [ - "./node_modules/font-awesome/fonts/*", - "./node_modules/font-awesome/css/font-awesome.min.css" - ], - "base": "./node_modules/font-awesome" - }, - { - "name": "jquery", - "src": [ - "./node_modules/jquery/dist/jquery.min.js", - "./node_modules/jquery/dist/jquery.min.map" - ], - "base": "./node_modules/jquery/dist" - }, - { - "name": "jquery-ui", - "src": ["./node_modules/jquery-ui-dist/jquery-ui.min.js"], - "base": "./node_modules/jquery-ui-dist" - }, - { - "name": "jquery-ui-touch-punch", - "src": ["./node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.min.js"], - "base": "./node_modules/jquery-ui-touch-punch" - }, - { - "name": "lazyload-js", - "src": ["./node_modules/lazyload-js/lazyload.min.js"], - "base": "./node_modules/lazyload-js" - }, - { - "name": "moment", - "src": ["./node_modules/moment/min/moment.min.js"], - "base": "./node_modules/moment/min" - }, - { - "name": "moment", - "src": ["./node_modules/moment/locale/*.js"], - "base": "./node_modules/moment/locale" - }, - { - "name": "ng-file-upload", - "src": ["./node_modules/ng-file-upload/dist/ng-file-upload.min.js"], - "base": "./node_modules/ng-file-upload/dist" - }, - { - "name": "nouislider", - "src": [ - "./node_modules/nouislider/distribute/nouislider.min.js", - "./node_modules/nouislider/distribute/nouislider.min.css" - ], - "base": "./node_modules/nouislider/distribute" - }, - { - "name": "signalr", - "src": ["./node_modules/signalr/jquery.signalR.js"], - "base": "./node_modules/signalr" - }, - { - "name": "spectrum", - "src": [ - "./node_modules/spectrum-colorpicker/spectrum.js", - "./node_modules/spectrum-colorpicker/spectrum.css" - ], - "base": "./node_modules/spectrum-colorpicker" - }, - { - "name": "tinymce", - "src": [ - "./node_modules/tinymce/tinymce.min.js", - "./node_modules/tinymce/plugins/**", - "./node_modules/tinymce/skins/**", - "./node_modules/tinymce/themes/**" - ], - "base": "./node_modules/tinymce" - }, - { - "name": "typeahead.js", - "src": ["./node_modules/typeahead.js/dist/typeahead.bundle.min.js"], - "base": "./node_modules/typeahead.js/dist" - }, - { - "name": "underscore", - "src": ["node_modules/underscore/underscore-min.js"], - "base": "./node_modules/underscore" - } - ]; - - // add streams for node modules - nodeModules.forEach(module => { - stream.add( - gulp.src(module.src, - { base: module.base }) - .pipe(gulp.dest(root + targets.lib + "/" + module.name)) - ); - }); - - //copy over libs which are not on npm (/lib) - stream.add( - gulp.src(sources.globs.lib) - .pipe(gulp.dest(root + targets.lib)) - ); - - //Copies all static assets into /root / assets folder - //css, fonts and image files - stream.add( - gulp.src(sources.globs.assets) - .pipe(imagemin([ - imagemin.gifsicle({interlaced: true}), - imagemin.jpegtran({progressive: true}), - imagemin.optipng({optimizationLevel: 5}), - imagemin.svgo({ - plugins: [ - {removeViewBox: true}, - {cleanupIDs: false} - ] - }) - ])) - .pipe(gulp.dest(root + targets.assets)) - ); - - // Copies all the less files related to the preview into their folder - //these are not pre-processed as preview has its own less combiler client side - stream.add( - gulp.src("src/canvasdesigner/editors/*.less") - .pipe(gulp.dest(root + targets.assets + "/less")) - ); - - // Todo: check if we need these fileSize - stream.add( - gulp.src("src/views/propertyeditors/grid/config/*.*") - .pipe(gulp.dest(root + targets.views + "/propertyeditors/grid/config")) - ); - stream.add( - gulp.src("src/views/dashboard/default/*.jpg") - .pipe(gulp.dest(root + targets.views + "/dashboard/default")) - ); - - return stream; -}); - - -/************************** - * Copies all angular JS files into their seperate umbraco.*.js file - **************************/ -gulp.task('js', function () { - - //we run multiple streams, so merge them all together - var stream = new MergeStream(); - - stream.add( - gulp.src(sources.globs.js) - .pipe(gulp.dest(root + targets.js)) - ); - - _.forEach(sources.js, function (group) { - stream.add (processJs(group.files, group.out) ); - }); - - return stream; -}); - -gulp.task('less', function () { - - var stream = new MergeStream(); - - _.forEach(sources.less, function (group) { - stream.add( processLess(group.files, group.out) ); - }); - - return stream; -}); - - -gulp.task('views', function () { - - var stream = new MergeStream(); - - _.forEach(sources.views, function (group) { - - console.log("copying " + group.files + " to " + root + targets.views + group.folder) - - stream.add ( - gulp.src(group.files) - .pipe( gulp.dest(root + targets.views + group.folder) ) - ); - - }); - - return stream; -}); - - -gulp.task('watch', function () { - - var stream = new MergeStream(); - var watchInterval = 500; - - //Setup a watcher for all groups of javascript files - _.forEach(sources.js, function (group) { - - if(group.watch !== false){ - - stream.add( - - watch(group.files, { ignoreInitial: true, interval: watchInterval }, function (file) { - - console.info(file.path + " has changed, added to: " + group.out); - processJs(group.files, group.out); - - }) - - ); - - } - - }); - - stream.add( - //watch all less files and trigger the less task - watch(sources.globs.less, { ignoreInitial: true, interval: watchInterval }, function () { - gulp.run(['less']); - }) - ); - - //watch all views - copy single file changes - stream.add( - watch(sources.globs.views, { interval: watchInterval }) - .pipe(gulp.dest(root + targets.views)) - ); - - //watch all app js files that will not be merged - copy single file changes - stream.add( - watch(sources.globs.js, { interval: watchInterval }) - .pipe(gulp.dest(root + targets.js)) - ); - - return stream; -}); - -/************************** - * Build Backoffice UI API documentation - **************************/ -gulp.task('docs', [], function (cb) { - - var options = { - html5Mode: false, - startPage: '/api', - title: "Umbraco Backoffice UI API Documentation", - dest: 'docs/api', - styles: ['docs/umb-docs.css'], - image: "https://our.umbraco.com/assets/images/logo.svg" - } - - return gulpDocs.sections({ - api: { - glob: ['src/common/**/*.js', 'docs/src/api/**/*.ngdoc'], - api: true, - title: 'API Documentation' - } - }) - .pipe(gulpDocs.process(options)) - .pipe(gulp.dest('docs/api')); - cb(); -}); - -gulp.task('connect:docs', function (cb) { - connect.server({ - root: 'docs/api', - livereload: true, - fallback: 'docs/api/index.html', - port: 8880 - }); - cb(); -}); - -gulp.task('open:docs', function (cb) { - - var options = { - uri: 'http://localhost:8880/index.html' - }; - - gulp.src(__filename) - .pipe(open(options)); - cb(); -}); - -/************************** - * Build tests - **************************/ - - // Karma test -gulp.task('test:unit', function() { - new karmaServer({ - configFile: __dirname + "/test/config/karma.conf.js", - keepalive: true - }) - .start(); -}); - -gulp.task('test:e2e', function() { - new karmaServer({ - configFile: __dirname + "/test/config/e2e.js", - keepalive: true - }) - .start(); -}); +'use strict'; + +/* + * gulpfile.js + * =========== + * Rather than manage one giant configuration file responsible + * for creating multiple tasks, each task has been broken out into + * its own file in gulp/tasks. Any file in that folder gets automatically + * required by the loop in ./gulp/index.js (required below). + * + * To add a new task, simply add a new task file to gulp/tasks. + */ + +require('./gulp'); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index f6d915eede..02ad091f75 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -5204,12 +5204,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5224,17 +5226,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5351,7 +5356,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5363,6 +5369,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5377,6 +5384,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5384,12 +5392,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5408,6 +5418,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5488,7 +5499,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5500,6 +5512,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5621,6 +5634,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index e35fdc3442..6e8159a940 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -2,7 +2,9 @@ "private": true, "scripts": { "test": "karma start test/config/karma.conf.js --singlerun", - "build": "gulp" + "build": "gulp build", + "dev": "gulp dev", + "docs": "gulp docs" }, "dependencies": { "ace-builds": "1.4.2", @@ -62,6 +64,7 @@ "gulp-watch": "5.0.1", "gulp-wrap": "0.14.0", "gulp-wrap-js": "0.4.1", + "gulp-notify": "^3.0.0", "jasmine-core": "3.3.0", "karma": "3.1.1", "karma-jasmine": "2.0.1", @@ -70,6 +73,7 @@ "lodash": "4.17.11", "marked": "^0.5.2", "merge-stream": "1.0.1", - "run-sequence": "2.2.1" + "run-sequence": "2.2.1", + "fs": "0.0.2" } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 1987c897f0..99d6b939e0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -215,9 +215,15 @@ * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - var selectedCulture = selectedVariant.language.culture; + //Find the whole variant model based on the culture that was chosen + var variant = _.find(vm.content.variants, function (v) { + return v.language.culture === selectedCulture; + }); + + insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); + //only the content app can be selected since no other apps are shown, and because we copy all of these apps //to the "editors" we need to update this across all editors for (var e = 0; e < vm.editors.length; e++) { @@ -233,13 +239,6 @@ } } - //Find the whole variant model based on the culture that was chosen - var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === selectedCulture; - }); - - insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); - //TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular editor.collapsed = true; editor.loading = true; @@ -260,6 +259,8 @@ vm.editors.splice(editorIndex, 1); //remove variant from open variants vm.openVariants.splice(editorIndex, 1); + //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) + $location.search("cculture", vm.openVariants[0]); splitViewChanged(); }, 400); } @@ -270,7 +271,7 @@ * @param {any} editorIndex The index of the editor being changed */ function selectVariant(variant, editorIndex) { - + // prevent variants already open in a split view to be opened if(vm.openVariants.indexOf(variant.language.culture) !== -1) { return; 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 index 56a18a217e..2bc1c636b5 100644 --- 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 @@ -44,6 +44,7 @@ the directive will use {@link umbraco.directives.directive:umbLockedField umbLoc @param {string} alias (binding): The model where the alias is bound. @param {string} aliasFrom (binding): The model to generate the alias from. +@param {string} validationPosition (binding): The position of the validation. Set to 'left' or 'right'. @param {boolean=} enableLock (binding): Set to true to add a lock next to the alias from where it can be unlocked and changed. **/ @@ -57,6 +58,7 @@ angular.module("umbraco.directives") alias: '=', aliasFrom: '=', enableLock: '=?', + validationPosition: '=?', serverValidationField: '@' }, link: function (scope, element, attrs, ctrl) { 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 index cc5a1eb2b1..f19e2c810b 100644 --- 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 @@ -40,6 +40,7 @@ Use this directive to render a value with a lock next to it. When the lock is cl @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} validationPosition (binding): The position of the validation. Set to 'left' or 'right'. @param {string=} serverValidationField (attribute): Set a server validation field. **/ @@ -70,7 +71,11 @@ Use this directive to render a value with a lock next to it. When the lock is cl // if locked state is not defined as an attr set default state if (scope.placeholderText === undefined || scope.placeholderText === null) { scope.placeholderText = "Enter value..."; - } + } + + if (scope.validationPosition === undefined || scope.validationPosition === null) { + scope.validationPosition = "left"; + } } @@ -93,9 +98,10 @@ Use this directive to render a value with a lock next to it. When the lock is cl templateUrl: 'views/components/umb-locked-field.html', scope: { ngModel: "=", - locked: "=?", + locked: "=?", placeholderText: "=?", - regexValidation: "=?", + regexValidation: "=?", + validationPosition: "=?", serverValidationField: "@" }, link: link 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 9724c222b6..6bc7855528 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 @@ -11,24 +11,24 @@ angular.module('umbraco.mocks'). var menu = [ { name: "Create", cssclass: "plus", alias: "create", metaData: {} }, - { seperator: true, name: "Delete", cssclass: "remove", alias: "delete", metaData: {} }, + { separator: true, name: "Delete", cssclass: "remove", alias: "delete", metaData: {} }, { name: "Move", cssclass: "move", alias: "move", metaData: {} }, { name: "Copy", cssclass: "copy", alias: "copy", metaData: {} }, { name: "Sort", cssclass: "sort", alias: "sort", metaData: {} }, - { seperator: true, name: "Publish", cssclass: "globe", alias: "publish", metaData: {} }, + { separator: true, name: "Publish", cssclass: "globe", alias: "publish", metaData: {} }, { name: "Rollback", cssclass: "undo", alias: "rollback", metaData: {} }, - { seperator: true, name: "Permissions", cssclass: "lock", alias: "permissions", metaData: {} }, + { separator: true, name: "Permissions", cssclass: "lock", alias: "permissions", metaData: {} }, { name: "Audit Trail", cssclass: "time", alias: "audittrail", metaData: {} }, { name: "Notifications", cssclass: "envelope", alias: "notifications", metaData: {} }, - { seperator: true, name: "Hostnames", cssclass: "home", alias: "hostnames", metaData: {} }, + { separator: true, name: "Hostnames", cssclass: "home", alias: "hostnames", metaData: {} }, { name: "Public Access", cssclass: "group", alias: "publicaccess", metaData: {} }, - { seperator: true, name: "Reload", cssclass: "refresh", alias: "users", metaData: {} }, + { separator: true, name: "Reload", cssclass: "refresh", alias: "users", metaData: {} }, - { seperator: true, name: "Empty Recycle Bin", cssclass: "trash", alias: "emptyrecyclebin", metaData: {} } + { separator: true, name: "Empty Recycle Bin", cssclass: "trash", alias: "emptyrecyclebin", metaData: {} } ]; var result = { @@ -94,7 +94,7 @@ angular.module('umbraco.mocks'). jsAction: "umbracoMenuActions.CreateChildEntity" } }, - { seperator: true, name: "Reload", cssclass: "refresh", alias: "users", metaData: {} } + { separator: true, name: "Reload", cssclass: "refresh", alias: "users", metaData: {} } ]; return [200, menu, null]; 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 ec1175ab6c..5c74971593 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 @@ -640,7 +640,7 @@ angular.module('umbraco.mocks'). "templateEditor_urlEncodeHelp": "Will format special characters in URLs", "templateEditor_usedIfAllEmpty": "Will only be used when the field values above are empty", "templateEditor_usedIfEmpty": "This field will only be used if the primary field is empty", - "templateEditor_withTime": "Yes, with time. Seperator: ", + "templateEditor_withTime": "Yes, with time. Separator: ", "translation_details": "Translation details", "translation_DownloadXmlDTD": "Download xml DTD", "translation_fields": "Fields", 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 47e5a24180..1258ec4099 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 @@ -421,7 +421,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica */ reBindChangedProperties: function (origContent, savedContent) { - //TODO: We should probably split out this logic to deal with media/members seperately to content + //TODO: We should probably split out this logic to deal with media/members separately to content //a method to ignore built-in prop changes var shouldIgnore = function (propName) { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 4705b8cc3e..bd493cf0d3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -125,6 +125,8 @@ body.touch .umb-tree { position: inherit; display: inherit; + list-style: none; + h6 { padding: 10px 0 10px 20px; font-weight: inherit; @@ -137,7 +139,15 @@ body.touch .umb-tree { } &-item { - padding-left: 20px; + padding: 4px 0; + + &:hover { + background-color: @gray-10; + } + + &-link { + display: block; + } } &-link { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index 0cb7bc9fe4..ba04069c6b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -28,7 +28,7 @@ color: @black; } -.umb-breadcrumbs__seperator { +.umb-breadcrumbs__separator { position: relative; top: 1px; margin-left: 5px; @@ -41,4 +41,4 @@ input.umb-breadcrumbs__add-ancestor { margin-top: -2px; margin-left: 3px; width: 100px; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less index 9001f1d473..8d9ae86ce7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less @@ -38,6 +38,7 @@ input.umb-locked-field__input { transition: color 0.25s; padding: 0; height: auto; + max-width: 300px; } input.umb-locked-field__input:focus { 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 index 417447c3dc..9b4bac723b 100644 --- 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 @@ -1,46 +1,67 @@ .umb-validation-label { - position: absolute; - top: 27px; - width: 200px; - padding: 1px 5px; - background: @red; - color: @white; - font-size: 11px; - line-height: 1.5em; + position: absolute; + top: 27px; + min-width: 100px; + max-width: 200px; + padding: 1px 5px; + background: @red; + color: @white; + font-size: 11px; + 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; + 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.-left { + left: 0; + right: auto; + + &:after { + left: 10px; + right: auto; + } +} + +.umb-validation-label.-right { + right: 0; + left: auto; + + &:after { + right: 10px; + left: auto; + } } .umb-validation-label.-arrow-left { - margin-left: 10px; + 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; + 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/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 3d9df196d7..ae930d6fb0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -373,10 +373,6 @@ margin-bottom: 0; } -.umb-panel-header-alias .umb-validation-label:after { - visibility: hidden; -} - .umb-panel-header-alias .umb-locked-field:after { display: none; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 81dfcfd5d3..7ae464d813 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -33,31 +33,51 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.showTarget = $scope.model.hideTarget !== true; + // this ensures that we only sync the tree once and only when it's ready + var oneTimeTreeSync = { + executed: false, + treeReady: false, + sync: function () { + // don't run this if: + // - it was already run once + // - the tree isn't ready yet + // - the model path hasn't been loaded yet + if (this.executed || !this.treeReady || !($scope.model.target && $scope.model.target.path)) { + return; + } + + this.executed = true; + // sync the tree to the model path + $scope.dialogTreeApi.syncTree({ + path: $scope.model.target.path, + tree: "content" + }); + } + }; + if (dialogOptions.currentTarget) { - $scope.model.target = dialogOptions.currentTarget; + // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target + $scope.model.target = angular.copy(dialogOptions.currentTarget); //if we have a node ID, we fetch the current node to build the form data if ($scope.model.target.id || $scope.model.target.udi) { //will be either a udi or an int var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; - if (!$scope.model.target.path) { - + // is it a content link? + if (!$scope.model.target.isMedia) { + // get the content path entityResource.getPath(id, "Document").then(function (path) { $scope.model.target.path = path; - //now sync the tree to this path - $scope.dialogTreeApi.syncTree({ - path: $scope.model.target.path, - tree: "content" - }); + oneTimeTreeSync.sync(); + }); + + // get the content properties to build the anchor name list + contentResource.getById(id).then(function (resp) { + $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); + $scope.model.target.url = resp.urls[0]; }); } - - // if a link exists, get the properties to build the anchor name list - contentResource.getById(id).then(function (resp) { - $scope.anchorValues = tinyMceService.getAnchorNames(JSON.stringify(resp.properties)); - $scope.model.target.url = resp.urls[0]; - }); } else if ($scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? @@ -72,6 +92,11 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.anchorValues = dialogOptions.anchors; } + function treeLoadedHandler(args) { + oneTimeTreeSync.treeReady = true; + oneTimeTreeSync.sync(); + } + function nodeSelectHandler(args) { if (args && args.event) { args.event.preventDefault(); @@ -127,6 +152,12 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", $scope.model.target.url = mediaHelper.resolveFile(media); editorService.close(); + + // make sure the content tree has nothing highlighted + $scope.dialogTreeApi.syncTree({ + path: "-1", + tree: "content" + }); }, close: function() { editorService.close(); @@ -146,7 +177,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", // method to select a search result $scope.selectResult = function (evt, result) { result.selected = result.selected === true ? false : true; - nodeSelectHandler(evt, { + nodeSelectHandler({ event: evt, node: result }); @@ -159,6 +190,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }; $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); } @@ -166,7 +198,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { + nodeSelectHandler({ node: node }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index cac412df8b..b5dd23b1fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -55,12 +55,12 @@
  • Media - / + /
  • {{item.name}} - / + /
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 1a4cc2b930..346bddf00f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -54,6 +54,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", vm.toggleLanguageSelector = toggleLanguageSelector; vm.selectLanguage = selectLanguage; vm.onSearchResults = onSearchResults; + vm.selectResult = selectResult; vm.hideSearch = hideSearch; vm.closeMiniListView = closeMiniListView; vm.selectListViewNode = selectListViewNode; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 2e7048eefa..92da12c423 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -5,7 +5,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 index 267d805d0a..3754b66c53 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -45,6 +45,7 @@ alias="$parent.alias" alias-from="$parent.name" enable-lock="true" + validation-position="'right'" server-validation-field="Alias"> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html index 032e4cd6c3..54a33a705f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -9,7 +9,7 @@ - + {{action.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 2e09fbfa3b..4004c2b958 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -5,16 +5,16 @@ -
    -
    Required alias
    -
    Invalid alias
    -
    {{lockedFieldForm.lockedField.errorMsg}}
    +
    + Required alias +
    +
    + Invalid alias +
    +
    {{lockedFieldForm.lockedField.errorMsg}} +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js index a64d6eed66..2c615db4dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js @@ -1,26 +1,91 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.RestoreController", - function ($scope, relationResource, mediaResource, navigationService, appState, treeService, localizationService) { + function ($scope, relationResource, mediaResource, entityResource, navigationService, appState, treeService, userService) { $scope.source = _.clone($scope.currentNode); $scope.error = null; - $scope.success = false; $scope.loading = true; + $scope.moving = false; + $scope.success = false; + + $scope.dialogTreeApi = {}; + $scope.searchInfo = { + showSearch: false, + results: [], + selectedSearchResults: [] + } + $scope.treeModel = { + hideHeader: false + } + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); + + function nodeSelectHandler(args) { + + if (args && args.event) { + 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; + + } + + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $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.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); + } + + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } relationResource.getByChildId($scope.source.id, "relateParentDocumentOnDelete").then(function (data) { $scope.loading = false; if (!data.length) { - localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_noRestoreRelation"]) - .then(function(values) { - $scope.success = false; - $scope.error = { - errorMsg: values[0], - data: { - Message: values[1] - } - } - }); + $scope.moving = true; return; } @@ -30,40 +95,31 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.RestoreController", $scope.target = { id: -1, name: "Root" }; } else { - $scope.loading = true; - mediaResource.getById($scope.relation.parentId).then(function (data) { - $scope.loading = false; - $scope.target = data; - // make sure the target item isn't in the recycle bin - if ($scope.target.path.indexOf("-21") !== -1) { - localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_restoreUnderRecycled"]) - .then(function (values) { - $scope.success = false; - $scope.error = { - errorMsg: values[0], - data: { - Message: values[1].replace('%0%', $scope.target.name) - } - } - }); - $scope.success = false; - } + $scope.loading = true; - }, function (err) { - $scope.success = false; - $scope.error = err; + entityResource.getById($scope.relation.parentId, "media").then(function (data) { $scope.loading = false; - }); + $scope.target = data; + + // make sure the target item isn't in the recycle bin + if ($scope.target.path.indexOf("-21") !== -1) { + $scope.moving = true; + $scope.target = null; + } + }, function (err) { + $scope.loading = false; + $scope.error = err; + }); } }, function (err) { - $scope.success = false; - $scope.error = err; $scope.loading = false; + $scope.error = err; }); $scope.restore = function () { - $scope.loading = true; + $scope.loading = true; + // this code was copied from `content.move.controller.js` mediaResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { @@ -89,9 +145,8 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.RestoreController", }); }, function (err) { - $scope.success = false; - $scope.error = err; $scope.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/media/restore.html b/src/Umbraco.Web.UI.Client/src/views/media/restore.html index bfc03128a3..6022caa251 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/restore.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/restore.html @@ -1,34 +1,87 @@
    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 c84644d251..1e7e3fa0ed 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 @@ -437,7 +437,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs view: "views/propertyeditors/listview/overlays/listviewpublish.html", submitButtonLabelKey: "actions_publish", submit: function (model) { - // create a comma seperated array of selected cultures + // create a comma separated array of selected cultures let selectedCultures = []; if (model.languages && model.languages.length > 0) { model.languages.forEach(language => { @@ -492,7 +492,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs submitButtonLabelKey: "actions_unpublish", submit: function (model) { - // create a comma seperated array of selected cultures + // create a comma separated array of selected cultures let selectedCultures = []; if (model.languages && model.languages.length > 0) { model.languages.forEach(language => { diff --git a/src/Umbraco.Web/Editors/TemplateController.cs b/src/Umbraco.Web/Editors/TemplateController.cs index 74cb2f0167..5c0a746ee7 100644 --- a/src/Umbraco.Web/Editors/TemplateController.cs +++ b/src/Umbraco.Web/Editors/TemplateController.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.Editors continue; } - //Find position in current comma seperate string path (so we get the correct children path) + //Find position in current comma separate string path (so we get the correct children path) var positionInPath = childTemplate.Path.IndexOf(templateIdInPath) + templateIdInPath.Length; //Get the substring of the child & any children (descendants it may have too) diff --git a/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs index 2e66b270c9..8c89bf263e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/CodeFileDisplay.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Path represents the path used by the backoffice tree - /// For files stored on disk, this is a urlencoded, comma seperated + /// For files stored on disk, this is a urlencoded, comma separated /// path to the file, always starting with -1. /// /// -1,Partials,Parials%2FFolder,Partials%2FFolder%2FFile.cshtml diff --git a/src/Umbraco.Web/Models/Trees/CreateChildEntity.cs b/src/Umbraco.Web/Models/Trees/CreateChildEntity.cs index 022056c35d..03bac2cfec 100644 --- a/src/Umbraco.Web/Models/Trees/CreateChildEntity.cs +++ b/src/Umbraco.Web/Models/Trees/CreateChildEntity.cs @@ -10,18 +10,18 @@ namespace Umbraco.Web.Models.Trees { public override string AngularServiceName => "umbracoMenuActions"; - public CreateChildEntity(string name, bool seperatorBefore = false) + public CreateChildEntity(string name, bool separatorBefore = false) : base(ActionNew.ActionAlias, name) { Icon = "add"; Name = name; - SeperatorBefore = seperatorBefore; + SeparatorBefore = separatorBefore; } - public CreateChildEntity(ILocalizedTextService textService, bool seperatorBefore = false) + public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) : base(ActionNew.ActionAlias, textService) { Icon = "add"; - SeperatorBefore = seperatorBefore; + SeparatorBefore = separatorBefore; } } } diff --git a/src/Umbraco.Web/Models/Trees/MenuItem.cs b/src/Umbraco.Web/Models/Trees/MenuItem.cs index 4170cdb73f..89239f43f7 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItem.cs @@ -49,7 +49,7 @@ namespace Umbraco.Web.Models.Trees { Name = name.IsNullOrWhiteSpace() ? action.Alias : name; Alias = action.Alias; - SeperatorBefore = false; + SeparatorBefore = false; Icon = action.Icon; Action = action; } @@ -80,8 +80,8 @@ namespace Umbraco.Web.Models.Trees /// /// Ensures a menu separator will exist before this menu item /// - [DataMember(Name = "seperator")] - public bool SeperatorBefore { get; set; } + [DataMember(Name = "separator")] + public bool SeparatorBefore { get; set; } [DataMember(Name = "cssclass")] public string Icon { get; set; } diff --git a/src/Umbraco.Web/Models/Trees/MenuItemList.cs b/src/Umbraco.Web/Models/Trees/MenuItemList.cs index 70b35e25bd..1df486ebdf 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItemList.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItemList.cs @@ -83,7 +83,7 @@ namespace Umbraco.Web.Models.Trees if (item == null) return null; var menuItem = new MenuItem(item, name) { - SeperatorBefore = hasSeparator, + SeparatorBefore = hasSeparator, OpensDialog = opensDialog }; @@ -98,7 +98,7 @@ namespace Umbraco.Web.Models.Trees var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}")) { - SeperatorBefore = hasSeparator, + SeparatorBefore = hasSeparator, OpensDialog = opensDialog }; diff --git a/src/Umbraco.Web/Models/Trees/RefreshNode.cs b/src/Umbraco.Web/Models/Trees/RefreshNode.cs index 2641baa34f..def753140e 100644 --- a/src/Umbraco.Web/Models/Trees/RefreshNode.cs +++ b/src/Umbraco.Web/Models/Trees/RefreshNode.cs @@ -10,18 +10,18 @@ namespace Umbraco.Web.Models.Trees { public override string AngularServiceName => "umbracoMenuActions"; - public RefreshNode(string name, bool seperatorBefore = false) + public RefreshNode(string name, bool separatorBefore = false) : base("refreshNode", name) { Icon = "refresh"; - SeperatorBefore = seperatorBefore; + SeparatorBefore = separatorBefore; } - public RefreshNode(ILocalizedTextService textService, bool seperatorBefore = false) + public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) : base("refreshNode", textService) { Icon = "refresh"; - SeperatorBefore = seperatorBefore; + SeparatorBefore = separatorBefore; } } } diff --git a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs index 58e71250eb..b917145dbd 100644 --- a/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MemberGroupPickerPropertyEditor.cs @@ -4,7 +4,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [DataEditor(Constants.PropertyEditors.Aliases.MemberGroupPicker, "Member Group Picker", "membergrouppicker", Group="People", Icon="icon-users")] + [DataEditor(Constants.PropertyEditors.Aliases.MemberGroupPicker, "Member Group Picker", "membergrouppicker", ValueType = ValueTypes.Text, Group = "People", Icon = "icon-users")] public class MemberGroupPickerPropertyEditor : DataEditor { public MemberGroupPickerPropertyEditor(ILogger logger) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs index ea38af314f..e040e7e926 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs @@ -154,9 +154,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public override string Name => _name; - public override PublishedCultureInfo GetCulture(string culture = null) => throw new NotSupportedException(); + public override PublishedCultureInfo GetCulture(string culture = null) => null; - public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); + private static readonly Lazy> NoCultures = new Lazy>(() => new Dictionary()); + public override IReadOnlyDictionary Cultures => NoCultures.Value; public override string UrlSegment => _urlName; diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs index a1fda17df1..a313d6947d 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlPublishedContent.cs @@ -136,9 +136,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public override PublishedCultureInfo GetCulture(string culture = null) => throw new NotSupportedException(); + public override PublishedCultureInfo GetCulture(string culture = null) => null; - public override IReadOnlyDictionary Cultures => throw new NotSupportedException(); + private static readonly Lazy> NoCultures = new Lazy>(() => new Dictionary()); + public override IReadOnlyDictionary Cultures => NoCultures.Value; public override string WriterName { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d4494c5f91..0ada3d8c5b 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -4,9 +4,7 @@ using System.Data; using System.Linq; using System.Web; using Examine; -using Examine.Search; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -268,6 +266,21 @@ namespace Umbraco.Web public static bool HasCulture(this IPublishedContent content, string culture) => content.Cultures.ContainsKey(culture); + /// + /// Filters a sequence of to return invariant items, and items that are published for the specified culture. + /// + /// The content items. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null). + internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, string culture = null) + { + if (contents == null) throw new ArgumentNullException(nameof(contents)); + + culture = culture ?? Current.VariationContextAccessor.VariationContext?.Culture ?? ""; + + // either does not vary by culture, or has the specified culture + return contents.Where(x => !x.ContentType.VariesByCulture() || x.HasCulture(culture)); + } + #endregion #region Search @@ -767,27 +780,29 @@ namespace Umbraco.Web /// /// /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// /// /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot /// - public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, string docTypeAlias) + public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, string docTypeAlias, string culture = null) { - return parentNodes.SelectMany(x => x.DescendantsOrSelf(docTypeAlias)); + return parentNodes.SelectMany(x => x.DescendantsOrSelf(docTypeAlias, culture)); } /// /// Returns all DescendantsOrSelf of all content referenced /// /// + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// /// /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot /// - public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes) + public static IEnumerable DescendantsOrSelf(this IEnumerable parentNodes, string culture = null) where T : class, IPublishedContent { - return parentNodes.SelectMany(x => x.DescendantsOrSelf()); + return parentNodes.SelectMany(x => x.DescendantsOrSelf(culture)); } @@ -810,133 +825,133 @@ namespace Umbraco.Web // - the relative order of siblings is the order in which they occur in the children property of their parent node. // - children and descendants occur before following siblings. - public static IEnumerable Descendants(this IPublishedContent content) + public static IEnumerable Descendants(this IPublishedContent content, string culture = null) { - return content.DescendantsOrSelf(false, null); + return content.DescendantsOrSelf(false, null, culture); } - public static IEnumerable Descendants(this IPublishedContent content, int level) + public static IEnumerable Descendants(this IPublishedContent content, int level, string culture = null) { - return content.DescendantsOrSelf(false, p => p.Level >= level); + return content.DescendantsOrSelf(false, p => p.Level >= level, culture); } - public static IEnumerable Descendants(this IPublishedContent content, string contentTypeAlias) + public static IEnumerable Descendants(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.DescendantsOrSelf(false, p => p.ContentType.Alias == contentTypeAlias); + return content.DescendantsOrSelf(false, p => p.ContentType.Alias == contentTypeAlias, culture); } - public static IEnumerable Descendants(this IPublishedContent content) + public static IEnumerable Descendants(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { - return content.Descendants().OfType(); + return content.Descendants(culture).OfType(); } - public static IEnumerable Descendants(this IPublishedContent content, int level) + public static IEnumerable Descendants(this IPublishedContent content, int level, string culture = null) where T : class, IPublishedContent { - return content.Descendants(level).OfType(); + return content.Descendants(level, culture).OfType(); } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content) + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string culture = null) { - return content.DescendantsOrSelf(true, null); + return content.DescendantsOrSelf(true, null, culture); } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level) + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string culture = null) { - return content.DescendantsOrSelf(true, p => p.Level >= level); + return content.DescendantsOrSelf(true, p => p.Level >= level, culture); } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string contentTypeAlias) + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.DescendantsOrSelf(true, p => p.ContentType.Alias == contentTypeAlias); + return content.DescendantsOrSelf(true, p => p.ContentType.Alias == contentTypeAlias, culture); } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content) + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { - return content.DescendantsOrSelf().OfType(); + return content.DescendantsOrSelf(culture).OfType(); } - public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level) + public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string culture = null) where T : class, IPublishedContent { - return content.DescendantsOrSelf(level).OfType(); + return content.DescendantsOrSelf(level, culture).OfType(); } - public static IPublishedContent Descendant(this IPublishedContent content) + public static IPublishedContent Descendant(this IPublishedContent content, string culture = null) { - return content.Children.FirstOrDefault(); + return content.Children(culture).FirstOrDefault(); } - public static IPublishedContent Descendant(this IPublishedContent content, int level) + public static IPublishedContent Descendant(this IPublishedContent content, int level, string culture = null) { - return content.EnumerateDescendants(false).FirstOrDefault(x => x.Level == level); + return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x.Level == level); } - public static IPublishedContent Descendant(this IPublishedContent content, string contentTypeAlias) + public static IPublishedContent Descendant(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.EnumerateDescendants(false).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); } - public static T Descendant(this IPublishedContent content) + public static T Descendant(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { - return content.EnumerateDescendants(false).FirstOrDefault(x => x is T) as T; + return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x is T) as T; } - public static T Descendant(this IPublishedContent content, int level) + public static T Descendant(this IPublishedContent content, int level, string culture = null) where T : class, IPublishedContent { - return content.Descendant(level) as T; + return content.Descendant(level, culture) as T; } - public static IPublishedContent DescendantOrSelf(this IPublishedContent content) + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string culture = null) { return content; } - public static IPublishedContent DescendantOrSelf(this IPublishedContent content, int level) + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, int level, string culture = null) { - return content.EnumerateDescendants(true).FirstOrDefault(x => x.Level == level); + return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x.Level == level); } - public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string contentTypeAlias) + public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.EnumerateDescendants(true).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); } - public static T DescendantOrSelf(this IPublishedContent content) + public static T DescendantOrSelf(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { - return content.EnumerateDescendants(true).FirstOrDefault(x => x is T) as T; + return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x is T) as T; } - public static T DescendantOrSelf(this IPublishedContent content, int level) + public static T DescendantOrSelf(this IPublishedContent content, int level, string culture = null) where T : class, IPublishedContent { - return content.DescendantOrSelf(level) as T; + return content.DescendantOrSelf(level, culture) as T; } - internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, bool orSelf, Func func) + internal static IEnumerable DescendantsOrSelf(this IPublishedContent content, bool orSelf, Func func, string culture = null) { - return content.EnumerateDescendants(orSelf).Where(x => func == null || func(x)); + return content.EnumerateDescendants(orSelf, culture).Where(x => func == null || func(x)); } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content, bool orSelf) + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, bool orSelf, string culture = null) { if (content == null) throw new ArgumentNullException(nameof(content)); if (orSelf) yield return content; - foreach (var desc in content.Children.SelectMany(x => x.EnumerateDescendants())) + foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants())) yield return desc; } - internal static IEnumerable EnumerateDescendants(this IPublishedContent content) + internal static IEnumerable EnumerateDescendants(this IPublishedContent content, string culture = null) { yield return content; - foreach (var desc in content.Children.SelectMany(x => x.EnumerateDescendants())) + foreach (var desc in content.Children(culture).SelectMany(x => x.EnumerateDescendants())) yield return desc; } @@ -1026,15 +1041,25 @@ namespace Umbraco.Web /// Gets the children of the content. /// /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// The children of the content. /// /// Children are sorted by their sortOrder. /// This method exists for consistency, it is the same as calling content.Children as a property. /// - public static IEnumerable Children(this IPublishedContent content) + public static IEnumerable Children(this IPublishedContent content, string culture = null) { if (content == null) throw new ArgumentNullException(nameof(content)); - return content.Children; + + // + return content.Children.Where(x => + { + if (!x.ContentType.VariesByCulture()) return true; // invariant = always ok + return x.HasCulture(culture); + return false; + }); + + return content.Children.WhereIsInvariantOrHasCulture(culture); } /// @@ -1042,24 +1067,26 @@ namespace Umbraco.Web /// /// The content. /// The predicate. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// The children of the content, filtered by the predicate. /// /// Children are sorted by their sortOrder. /// - public static IEnumerable Children(this IPublishedContent content, Func predicate) + public static IEnumerable Children(this IPublishedContent content, Func predicate, string culture = null) { - return content.Children().Where(predicate); + return content.Children(culture).Where(predicate); } /// /// Gets the children of the content, of any of the specified types. /// /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// One or more content type alias. /// The children of the content, of any of the specified types. - public static IEnumerable Children(this IPublishedContent content, params string[] alias) + public static IEnumerable Children(this IPublishedContent content, string culture = null, params string[] alias) { - return content.Children(x => alias.InvariantContains(x.ContentType.Alias)); + return content.Children(x => alias.InvariantContains(x.ContentType.Alias), culture); } /// @@ -1067,19 +1094,20 @@ namespace Umbraco.Web /// /// The content type. /// The content. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// The children of content, of the given content type. /// /// Children are sorted by their sortOrder. /// - public static IEnumerable Children(this IPublishedContent content) + public static IEnumerable Children(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { - return content.Children().OfType(); + return content.Children(culture).OfType(); } - public static IPublishedContent FirstChild(this IPublishedContent content) + public static IPublishedContent FirstChild(this IPublishedContent content, string culture = null) { - return content.Children().FirstOrDefault(); + return content.Children(culture).FirstOrDefault(); } /// @@ -1088,26 +1116,26 @@ namespace Umbraco.Web /// The content. /// The content type alias. /// The first child of content, of the given content type. - public static IPublishedContent FirstChild(this IPublishedContent content, string alias) + public static IPublishedContent FirstChild(this IPublishedContent content, string alias, string culture = null) { - return content.Children(alias).FirstOrDefault(); + return content.Children(culture,alias).FirstOrDefault(); } - public static IPublishedContent FirstChild(this IPublishedContent content, Func predicate) + public static IPublishedContent FirstChild(this IPublishedContent content, Func predicate, string culture = null) { - return content.Children(predicate).FirstOrDefault(); + return content.Children(predicate, culture).FirstOrDefault(); } - public static T FirstChild(this IPublishedContent content) + public static T FirstChild(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { - return content.Children().FirstOrDefault(); + return content.Children(culture).FirstOrDefault(); } - public static T FirstChild(this IPublishedContent content, Func predicate) + public static T FirstChild(this IPublishedContent content, Func predicate, string culture = null) where T : class, IPublishedContent { - return content.Children().FirstOrDefault(predicate); + return content.Children(culture).FirstOrDefault(predicate); } /// @@ -1116,10 +1144,11 @@ namespace Umbraco.Web /// The content. /// A service context. /// An optional content type alias. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// The children of the content. - public static DataTable ChildrenAsTable(this IPublishedContent content, ServiceContext services, string contentTypeAliasFilter = "") + public static DataTable ChildrenAsTable(this IPublishedContent content, ServiceContext services, string contentTypeAliasFilter = "", string culture = null) { - return GenerateDataTable(content, services, contentTypeAliasFilter); + return GenerateDataTable(content, services, contentTypeAliasFilter, culture); } /// @@ -1128,14 +1157,15 @@ namespace Umbraco.Web /// The content. /// A service context. /// An optional content type alias. + /// The specific culture to filter for. If null is used the current culture is used. (Default is null) /// The children of the content. - private static DataTable GenerateDataTable(IPublishedContent content, ServiceContext services, string contentTypeAliasFilter = "") + private static DataTable GenerateDataTable(IPublishedContent content, ServiceContext services, string contentTypeAliasFilter = "", string culture = null) { var firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() - ? content.Children.Any() - ? content.Children.ElementAt(0) + ? content.Children(culture).Any() + ? content.Children(culture).ElementAt(0) : null - : content.Children.FirstOrDefault(x => x.ContentType.Alias == contentTypeAliasFilter); + : content.Children(culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAliasFilter); if (firstNode == null) return new DataTable(); //no children found diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 3d3b623838..b6c89b29ad 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -646,7 +646,30 @@ namespace Umbraco.Web.Routing } else { - _logger.Debug("EnsurePublishedContentAccess: Current member has access"); + // grab the current member + var member = membershipHelper.GetCurrentMember(); + // if the member has the "approved" and/or "locked out" properties, make sure they're correctly set before allowing access + var memberIsActive = true; + if (member != null) + { + if (member.HasProperty(Constants.Conventions.Member.IsApproved) == false) + memberIsActive = member.Value(Constants.Conventions.Member.IsApproved); + + if (member.HasProperty(Constants.Conventions.Member.IsLockedOut) == false) + memberIsActive = member.Value(Constants.Conventions.Member.IsLockedOut) == false; + } + + if (memberIsActive == false) + { + _logger.Debug("Current member is either unapproved or locked out, redirect to error page"); + var errorPageId = publicAccessAttempt.Result.NoAccessNodeId; + if (errorPageId != request.PublishedContent.Id) + request.PublishedContent = request.UmbracoContext.PublishedSnapshot.Content.GetById(errorPageId); + } + else + { + _logger.Debug("Current member has access"); + } } } else diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 3da8540fe3..46ee6cd8b5 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -137,7 +137,7 @@ namespace Umbraco.Web.Trees if (menu.Items.Any()) { - menu.Items.Last().SeperatorBefore = true; + menu.Items.Last().SeparatorBefore = true; } // add default actions for *all* users @@ -237,14 +237,16 @@ namespace Umbraco.Web.Trees AddActionNode(item, menu, opensDialog: true); //fixme - conver this editor to angular AddActionNode(item, menu, true, convert: true, opensDialog: true); - - menu.Items.Add(new MenuItem("notify", Services.TextService) + if (EmailSender.CanSendRequiredEmail) { - Icon = "megaphone", - SeperatorBefore = true, - OpensDialog = true - }); - + menu.Items.Add(new MenuItem("notify", Services.TextService) + { + Icon = "megaphone", + SeparatorBefore = true, + OpensDialog = true + }); + } + menu.Items.Add(new RefreshNode(Services.TextService, true)); return menu; diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index a4c820ae25..3c6b9c782c 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -84,7 +84,7 @@ namespace Umbraco.Web.Trees menu.Items.Add(new MenuItem("importDocumentType", Services.TextService) { Icon = "page-up", - SeperatorBefore = true, + SeparatorBefore = true, OpensDialog = true }); menu.Items.Add(new RefreshNode(Services.TextService, true)); @@ -130,7 +130,7 @@ namespace Umbraco.Web.Trees menu.Items.Add(new MenuItem("export", Services.TextService) { Icon = "download-alt", - SeperatorBefore = true, + SeparatorBefore = true, OpensDialog = true }); menu.Items.Add(Services.TextService, true, opensDialog: true); diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index f4be2a1700..b6ffdca105 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -832,7 +832,7 @@ namespace Umbraco.Web } /// - /// Joins any number of int/string/objects into one string and seperates them with the string seperator parameter. + /// Joins any number of int/string/objects into one string and separates them with the string separator parameter. /// public string Join(string separator, params object[] args) {