diff --git a/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md new file mode 100644 index 0000000000..7d2afb46bf --- /dev/null +++ b/.github/CONTRIBUTION_GUIDELINES.md @@ -0,0 +1,35 @@ +# Contributing to Umbraco CMS + +When you’re considering creating a pull request for Umbraco CMS, we will categorize them in two different sizes, small and large. + +The process for both sizes is very similar, as [explained in the contribution document](CONTRIBUTING.md#how-do-i-begin). + +## Small PRs +Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. + +We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process](REVIEW_PROCESS.md). + +### Up for grabs + +Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. + +If you do start working on something, make sure leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. + +## Large PRs +New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). + +We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. + +Please make sure to describe your idea in an issue, it helps to put in mockup screenshots or videos. + +If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. + +If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the [small PRs process](#small-prs) above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. + +It is highly recommended that you speak to the HQ before making large, complex changes. + +### Pull request or package? + +If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and to fix bugs. + +Eventually, a package could "graduate" to be included in the CMS. diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md new file mode 100644 index 0000000000..917d25b090 --- /dev/null +++ b/.github/REVIEW_PROCESS.md @@ -0,0 +1,25 @@ +# Review process + +You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response. + +In order to set some expectations, here's what happens next. + +## Review process + +You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. + +You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes: + +- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco +- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible +- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see +- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!) + +## Are you still available? + +We understand you have other things to do and can't just drop everything to help us out. +So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. + +If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. + +There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. \ No newline at end of file diff --git a/build/NuSpecs/tools/Readme.txt b/build/NuSpecs/tools/Readme.txt index e40b0dbc7e..b0583a2b4d 100644 --- a/build/NuSpecs/tools/Readme.txt +++ b/build/NuSpecs/tools/Readme.txt @@ -1,11 +1,11 @@ - 888 - 888 -888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. -888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b -888 888 888 888 888 888 888 888 .d888888 888 888 888 -Y88b 888 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P - "Y88888 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" + 888 + 888 +888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. +888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b +888 888 888 888 888 888 888 888 .d888888 888 888 888 +Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P + "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" ------------------------------------------------------------------ diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index df364c64ed..2f52d03776 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -1,12 +1,13 @@ - _ _ __ __ ____ _____ _____ ____ - | | | | \/ | _ \| __ \ /\ / ____/ __ \ - | | | | \ / | |_) | |__) | / \ | | | | | | - | | | | |\/| | _ <| _ / / /\ \| | | | | | - | |__| | | | | |_) | | \ \ / ____ | |___| |__| | - \____/|_| |_|____/|_| \_/_/ \_\_____\____/ + 888 + 888 +888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. +888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b +888 888 888 888 888 888 888 888 .d888888 888 888 888 +Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P + "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" ----------------------------------------------------- +------------------------------------------------------------------ Don't forget to build! diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs index 0baefe104b..d252c58730 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs @@ -96,7 +96,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions var pluginLangFolders = appPlugins.Exists == false ? Enumerable.Empty() : appPlugins.GetDirectories() - .SelectMany(x => x.GetDirectories("Lang")) + .SelectMany(x => x.GetDirectories("Lang", SearchOption.AllDirectories)) .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 0b5d2873f9..b55dc0ca18 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -96,6 +96,11 @@ namespace Umbraco.Core /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Multiple Media Picker. + /// + public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; + /// /// Member Picker. /// diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs index 66b788f9c8..f665aaa8ed 100644 --- a/src/Umbraco.Core/DateTimeExtensions.cs +++ b/src/Umbraco.Core/DateTimeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; @@ -15,7 +16,7 @@ namespace Umbraco.Core /// public static string ToIsoString(this DateTime dt) { - return dt.ToString("yyyy-MM-dd HH:mm:ss"); + return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); } public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 9b13457b76..df74bf7c87 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using NPoco; +using Umbraco.Core; using Umbraco.Core.Migrations.Expressions.Common; using Umbraco.Core.Persistence.SqlSyntax; @@ -27,31 +29,57 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.KeysAndIndexes { _context.BuildingExpression = false; + //get a list of all constraints - this will include all PK, FK and unique constraints + var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + + //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes + var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + + var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2); + var indexNames = indexes.Select(x => x.IndexName).ToList(); + // drop keys if (DeleteLocal || DeleteForeign) { // table, constraint - var tableKeys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + if (DeleteForeign) { - foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("FK_"))) + //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: + // * starts with FK OR + // * doesn't start with PK_ and doesn't exist in the list of indexes + + foreach (var key in tableConstraints.Where(x => x.Item1 == TableName + && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2))))) + { Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); + } + } if (DeleteLocal) { - foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("PK_"))) + foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - // note: we do *not* delete the DEFAULT constraints + // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways + // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn } } // drop indexes if (DeleteLocal) - { - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + { foreach (var index in indexes.Where(x => x.TableName == TableName)) - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + { + //if this is a unique constraint we need to drop the constraint, else drop the index + //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints + + if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) + Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); + else + Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + } + } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs new file mode 100644 index 0000000000..87a1fd6504 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + { + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "preview") + return preValue.Value == "1"; + + return base.GetPreValueValue(preValue); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs index 8dfa464508..db9021d653 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs @@ -20,7 +20,8 @@ public class PreValueMigratorComposer : ICoreComposer .Append() .Append() .Append() - .Append(); + .Append() + .Append(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs index bd08b53877..58ec0e30c2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs @@ -15,21 +15,17 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { //special trick to add the column without constraints and return the sql to add them later AddColumn("macroType", out var sqls1); - //now we need to update the new column with some values because this column doesn't allow NULL values - Update.Table(Constants.DatabaseSchema.Tables.Macro).Set(new { macroType = (int)MacroTypes.Unknown}).AllRows().Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls1) Execute.Sql(sql).Do(); - - //special trick to add the column without constraints and return the sql to add them later AddColumn("macroSource", out var sqls2); - //populate the new macroSource column with legacy data - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython IS NOT NULL").Do(); + //populate the new columns with legacy data + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = {(int)MacroTypes.Unknown}").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython != '' AND macroPython IS NOT NULL").Do(); //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls1) Execute.Sql(sql).Do(); foreach (var sql in sqls2) Execute.Sql(sql).Do(); //now remove these old columns diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 1b6597a660..b68aa23d78 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -50,10 +50,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var obj = JsonConvert.DeserializeObject(value); var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); - foreach (var control in allControls.SelectMany(c => c)) + foreach (var control in allControls.SelectMany(c => c).OfType()) { var controlValue = control["value"]; - if (controlValue.Type == JTokenType.String) + if (controlValue?.Type == JTokenType.String) { control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); } diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index c811f484bc..2f6b91edee 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using System.Web; using System.Xml.Linq; using System.Xml.XPath; @@ -575,12 +574,11 @@ namespace Umbraco.Core.Packaging contentType.Thumbnail = infoElement.Element("Thumbnail").Value; contentType.Description = infoElement.Element("Description").Value; - //NOTE AllowAtRoot is a new property in the package xml so we need to verify it exists before using it. + //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. var allowAtRoot = infoElement.Element("AllowAtRoot"); if (allowAtRoot != null) contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); - //NOTE IsListView is a new property in the package xml so we need to verify it exists before using it. var isListView = infoElement.Element("IsListView"); if (isListView != null) contentType.IsContainer = isListView.Value.InvariantEquals("true"); @@ -589,6 +587,10 @@ namespace Umbraco.Core.Packaging if (isElement != null) contentType.IsElement = isElement.Value.InvariantEquals("true"); + var variationsElement = infoElement.Element("Variations"); + if (variationsElement != null) + contentType.Variations = (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); + //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set var masterElement = infoElement.Element("Master"); if (masterElement != null) @@ -614,7 +616,7 @@ namespace Umbraco.Core.Packaging var compositionContentType = importedContentTypes.ContainsKey(compositionAlias) ? importedContentTypes[compositionAlias] : _contentTypeService.Get(compositionAlias); - var added = contentType.AddContentType(compositionContentType); + contentType.AddContentType(compositionContentType); } } } @@ -748,9 +750,14 @@ namespace Umbraco.Core.Packaging { Name = property.Element("Name").Value, Description = (string)property.Element("Description"), - Mandatory = property.Element("Mandatory") != null ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") : false, + Mandatory = property.Element("Mandatory") != null + ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") + : false, ValidationRegExp = (string)property.Element("Validation"), - SortOrder = sortOrder + SortOrder = sortOrder, + Variations = property.Element("Variations") != null + ? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value) + : ContentVariation.Nothing }; var tab = (string)property.Element("Tab"); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index c25328b10c..0788594e3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -174,7 +174,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.Id, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); // map the DateStamp for (var i = 0; i < items.Count; i++) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 4a32e373c1..b6d39fe54f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -404,7 +404,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isMedia) { sql - .InnerJoin().On((left, right) => left.Id == right.Id); + .LeftJoin().On((left, right) => left.Id == right.Id); } //Any LeftJoin statements need to come last diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 5492f97409..faf4744c76 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -757,11 +757,11 @@ namespace Umbraco.Core.Services.Implement { var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save (un)publishing content, use the dedicated SavePublished method."); + throw new InvalidOperationException($"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method."); if (content.Name != null && content.Name.Length > 255) { - throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + throw new InvalidOperationException($"Content with the name {content.Name} cannot be more than 255 characters in length."); } var evtMsgs = EventMessagesFactory.Get(); diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 863ecc0b1b..ace740a831 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -437,7 +437,8 @@ namespace Umbraco.Core.Services.Implement new XElement("Description", contentType.Description), new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()), new XElement("IsListView", contentType.IsContainer.ToString()), - new XElement("IsElement", contentType.IsElement.ToString())); + new XElement("IsElement", contentType.IsElement.ToString()), + new XElement("Variations", contentType.Variations.ToString())); var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId); if(masterContentType != null) @@ -487,7 +488,8 @@ namespace Umbraco.Core.Services.Implement new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, - propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null, + new XElement("Variations", propertyType.Variations.ToString())); genericProperties.Add(genericProperty); } diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 9cd4a58c65..2a558f85aa 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -97,8 +97,7 @@ namespace Umbraco.Core.Sync ? ":" + request.ServerVariables["SERVER_PORT"] : ""; - var useSsl = globalSettings.UseHttps || port == "443"; - var ssl = useSsl ? "s" : ""; // force, whatever the first request + var ssl = globalSettings.UseHttps ? "s" : ""; // force, whatever the first request var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); return url.TrimEnd('/'); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index bdbe384b05..c19722433f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -235,6 +235,7 @@ + diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 7459ae848b..9cd4f39c17 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(38, types.Count()); + Assert.AreEqual(39, types.Count()); } /// diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index 935417c510..135172460d 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Routing { base.SetUp(); - WebFinalComponent.CreateRoutes( + WebInitialComponent.CreateRoutes( new TestUmbracoContextAccessor(), TestObjects.GetGlobalSettings(), new SurfaceControllerTypeCollection(Enumerable.Empty()), diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js index e1617d8365..a75a7f1f3c 100644 --- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js +++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js @@ -1239,6 +1239,11 @@ // Perform the button's action. function doClick(button) { + // don't do anything if the editor input or button bar isn't the currently active element + if (document.activeElement !== panels.input && !panels.buttonBar.contains(document.activeElement)) { + return; + } + inputBox.focus(); if (button.textOp) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index 577ffe0176..4993b013c7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper) { function link(scope, element, attrs, ctrl) { @@ -28,6 +28,9 @@ // make sure dates are formatted to the user's locale formatDatesToLocal(); + + // set media file extension initially + setMediaExtension(); } function formatDatesToLocal() { @@ -49,6 +52,10 @@ } } + function setMediaExtension() { + scope.node.extension = mediaHelper.getFileExtension(scope.nodeUrl); + } + scope.openMediaType = function (mediaType) { var editor = { id: mediaType.id, @@ -62,6 +69,16 @@ editorService.mediaTypeEditor(editor); }; + scope.openSVG = function () { + var popup = window.open('', '_blank'); + var html = '' + + ''; + + popup.document.open(); + popup.document.write(html); + popup.document.close(); + } + // watch for content updates - reload content when node is saved, published etc. scope.$watch('node.updateDate', function(newValue, oldValue){ if(!newValue) { return; } @@ -72,6 +89,9 @@ // Update the create and update dates formatDatesToLocal(); + + //Update the media file format + setMediaExtension(); }); //ensure to unregister from all events! diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js similarity index 93% rename from src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js index 79b29b407b..899b8f3c23 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbflatpickr.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -1,13 +1,13 @@ /** @ngdoc directive -@name umbraco.directives.directive:umbFlatpickr +@name umbraco.directives.directive:umbDateTimePicker @restrict E @scope @description Added in Umbraco version 8.0 This directive is a wrapper of the flatpickr library. Use it to render a date time picker. -For extra details about options and events take a look here: https://flatpickr.js.org/ +For extra details about options and events take a look here: https://flatpickr.js.org Use this directive to render a date time picker @@ -15,11 +15,11 @@ Use this directive to render a date time picker
 	
- - +
@@ -70,12 +70,12 @@ Use this directive to render a date time picker (function() { 'use strict'; - var umbFlatpickr = { + var umbDateTimePicker = { template: '' + '' + '
' + '
', - controller: umbFlatpickrCtrl, + controller: umbDateTimePickerCtrl, transclude: true, bindings: { ngModel: '<', @@ -92,9 +92,9 @@ Use this directive to render a date time picker } }; - function umbFlatpickrCtrl($element, $timeout, $scope, assetsService, userService) { + function umbDateTimePickerCtrl($element, $timeout, $scope, assetsService, userService) { + var ctrl = this; - var loaded = false; var userLocale = null; ctrl.$onInit = function() { @@ -102,14 +102,14 @@ Use this directive to render a date time picker // load css file for the date picker assetsService.loadCss('lib/flatpickr/flatpickr.css', $scope).then(function () { userService.getCurrentUser().then(function (user) { + // init date picker userLocale = user.locale; if (userLocale.indexOf('-') > -1) { userLocale = userLocale.split('-')[0]; } - loaded = true; - grabElementAndRunFlatpickr(); + grabElementAndRunFlatpickr(); }); }); @@ -234,7 +234,8 @@ Use this directive to render a date time picker } } - - angular.module('umbraco.directives').component('umbFlatpickr', umbFlatpickr); - + + // umbFlatpickr (umb-flatpickr) is deprecated, but we keep it for backwards compatibility + angular.module('umbraco.directives').component('umbFlatpickr', umbDateTimePicker); + angular.module('umbraco.directives').component('umbDateTimePicker', umbDateTimePicker); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index f3217aa4de..3d638e6773 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -17,6 +17,7 @@ scope.sortableOptionsGroup = {}; scope.sortableOptionsProperty = {}; scope.sortingButtonKey = "general_reorder"; + scope.compositionsButtonState = "init"; function activate() { @@ -343,6 +344,7 @@ })), function(f) { return f !== null && f !== undefined; }); + scope.compositionsButtonState = "busy"; $q.all([ //get available composite types availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { @@ -363,6 +365,7 @@ scope.isCompositionsDialogLoading = false; //resolves when both other promises are done, now show it editorService.open(scope.compositionsDialogModel); + scope.compositionsButtonState = "init"; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js new file mode 100644 index 0000000000..e70f7b3cac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js @@ -0,0 +1,75 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbLoader +@restrict E + +@description +Use this directive to generate a loading indicator. + +

Markup example

+
+    
+ + + + +
+

{{content}}

+
+ +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+
+            vm.content = "";
+            vm.loading = true;
+
+            myService.getContent().then(function(content){
+                vm.content = content;
+                vm.loading = false;
+            });
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {string=} position The loader position ("top", "bottom"). + +**/ + +(function() { + 'use strict'; + + function UmbLoaderDirective() { + + function link(scope, el, attr, ctrl) { + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-loader.html', + scope: { + position: "@?" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLoader', UmbLoaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js index 0671770796..c45b8f3f47 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js @@ -39,7 +39,7 @@ Use this directive to generate a loading indicator. }); } -½ + angular.module("umbraco").controller("My.Controller", Controller); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index f8dbefa5d7..97eb2bf708 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -1,167 +1,172 @@ (function () { - 'use strict'; + 'use strict'; - function ChangePasswordController($scope) { + function ChangePasswordController($scope) { - function resetModel(isNew) { - //the model config will contain an object, if it does not we'll create defaults - //NOTE: We will not support doing the password regex on the client side because the regex on the server side - //based on the membership provider cannot always be ported to js from .net directly. - /* - { - hasPassword: true/false, - requiresQuestionAnswer: true/false, - enableReset: true/false, - enablePasswordRetrieval: true/false, - minPasswordLength: 10 - } - */ + var vm = this; - $scope.showReset = false; + vm.$onInit = onInit; + vm.$onDestroy = onDestroy; + vm.doChange = doChange; + vm.cancelChange = cancelChange; + vm.showOldPass = showOldPass; + vm.showCancelBtn = showCancelBtn; - //set defaults if they are not available - if ($scope.config.disableToggle === undefined) { - $scope.config.disableToggle = false; - } - if ($scope.config.hasPassword === undefined) { - $scope.config.hasPassword = false; - } - if ($scope.config.enablePasswordRetrieval === undefined) { - $scope.config.enablePasswordRetrieval = true; - } - if ($scope.config.requiresQuestionAnswer === undefined) { - $scope.config.requiresQuestionAnswer = false; - } - //don't enable reset if it is new - that doesn't make sense - if (isNew === "true") { - $scope.config.enableReset = false; - } - else if ($scope.config.enableReset === undefined) { - $scope.config.enableReset = true; - } - - if ($scope.config.minPasswordLength === undefined) { - $scope.config.minPasswordLength = 0; - } - - //set the model defaults - if (!angular.isObject($scope.passwordValues)) { - //if it's not an object then just create a new one - $scope.passwordValues = { - newPassword: null, - oldPassword: null, - reset: null, - answer: null - }; - } - else { - //just reset the values + var unsubscribe = []; + + function resetModel(isNew) { + //the model config will contain an object, if it does not we'll create defaults + //NOTE: We will not support doing the password regex on the client side because the regex on the server side + //based on the membership provider cannot always be ported to js from .net directly. + /* + { + hasPassword: true/false, + requiresQuestionAnswer: true/false, + enableReset: true/false, + enablePasswordRetrieval: true/false, + minPasswordLength: 10 + } + */ + + vm.showReset = false; + + //set defaults if they are not available + if (vm.config.disableToggle === undefined) { + vm.config.disableToggle = false; + } + if (vm.config.hasPassword === undefined) { + vm.config.hasPassword = false; + } + if (vm.config.enablePasswordRetrieval === undefined) { + vm.config.enablePasswordRetrieval = true; + } + if (vm.config.requiresQuestionAnswer === undefined) { + vm.config.requiresQuestionAnswer = false; + } + //don't enable reset if it is new - that doesn't make sense + if (isNew === "true") { + vm.config.enableReset = false; + } + else if (vm.config.enableReset === undefined) { + vm.config.enableReset = true; + } + + if (vm.config.minPasswordLength === undefined) { + vm.config.minPasswordLength = 0; + } + + //set the model defaults + if (!angular.isObject(vm.passwordValues)) { + //if it's not an object then just create a new one + vm.passwordValues = { + newPassword: null, + oldPassword: null, + reset: null, + answer: null + }; + } + else { + //just reset the values + + if (!isNew) { + //if it is new, then leave the generated pass displayed + vm.passwordValues.newPassword = null; + vm.passwordValues.oldPassword = null; + } + vm.passwordValues.reset = null; + vm.passwordValues.answer = null; + } + + //the value to compare to match passwords + if (!isNew) { + vm.passwordValues.confirm = ""; + } + else if (vm.passwordValues.newPassword && vm.passwordValues.newPassword.length > 0) { + //if it is new and a new password has been set, then set the confirm password too + vm.passwordValues.confirm = vm.passwordValues.newPassword; + } - if (!isNew) { - //if it is new, then leave the generated pass displayed - $scope.passwordValues.newPassword = null; - $scope.passwordValues.oldPassword = null; } - $scope.passwordValues.reset = null; - $scope.passwordValues.answer = null; - } - //the value to compare to match passwords - if (!isNew) { - $scope.passwordValues.confirm = ""; - } - else if ($scope.passwordValues.newPassword && $scope.passwordValues.newPassword.length > 0) { - //if it is new and a new password has been set, then set the confirm password too - $scope.passwordValues.confirm = $scope.passwordValues.newPassword; - } + //when the scope is destroyed we need to unsubscribe + function onDestroy() { + for (var u in unsubscribe) { + unsubscribe[u](); + } + } + + function onInit() { + //listen for the saved event, when that occurs we'll + //change to changing = false; + unsubscribe.push($scope.$on("formSubmitted", function () { + if (vm.config.disableToggle === false) { + vm.changing = false; + } + })); + + unsubscribe.push($scope.$on("formSubmitting", function () { + //if there was a previously generated password displaying, clear it + if (vm.changing && vm.passwordValues) { + vm.passwordValues.generatedPassword = null; + } + else if (!vm.changing) { + //we are not changing, so the model needs to be null + vm.passwordValues = null; + } + })); + + resetModel(vm.isNew); + + //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there + //with validators turned on. + vm.changing = vm.config.disableToggle === true || !vm.config.hasPassword; + + //we're not currently changing so set the model to null + if (!vm.changing) { + vm.passwordValues = null; + } + } + + function doChange() { + resetModel(); + vm.changing = true; + //if there was a previously generated password displaying, clear it + vm.passwordValues.generatedPassword = null; + vm.passwordValues.confirm = null; + }; + + function cancelChange() { + vm.changing = false; + //set model to null + vm.passwordValues = null; + }; + + function showOldPass() { + return vm.config.hasPassword && + !vm.config.allowManuallyChangingPassword && + !vm.config.enablePasswordRetrieval && !vm.showReset; + }; + + // TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this component + function showCancelBtn() { + return vm.config.disableToggle !== true && vm.config.hasPassword; + }; } - resetModel($scope.isNew); - - //if there is no password saved for this entity , it must be new so we do not allow toggling of the change password, it is always there - //with validators turned on. - $scope.changing = $scope.config.disableToggle === true || !$scope.config.hasPassword; - - //we're not currently changing so set the model to null - if (!$scope.changing) { - $scope.passwordValues = null; - } - - $scope.doChange = function () { - resetModel(); - $scope.changing = true; - //if there was a previously generated password displaying, clear it - $scope.passwordValues.generatedPassword = null; - $scope.passwordValues.confirm = null; + var component = { + templateUrl: 'views/components/users/change-password.html', + controller: ChangePasswordController, + controllerAs: 'vm', + bindings: { + isNew: "<", + passwordValues: "=", //TODO: Do we need bi-directional vals? + config: "=" //TODO: Do we need bi-directional vals? + //TODO: Do we need callbacks? + } }; - $scope.cancelChange = function () { - $scope.changing = false; - //set model to null - $scope.passwordValues = null; - }; - - var unsubscribe = []; - - //listen for the saved event, when that occurs we'll - //change to changing = false; - unsubscribe.push($scope.$on("formSubmitted", function () { - if ($scope.config.disableToggle === false) { - $scope.changing = false; - } - })); - unsubscribe.push($scope.$on("formSubmitting", function () { - //if there was a previously generated password displaying, clear it - if ($scope.changing && $scope.passwordValues) { - $scope.passwordValues.generatedPassword = null; - } - else if (!$scope.changing) { - //we are not changing, so the model needs to be null - $scope.passwordValues = null; - } - })); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - - $scope.showOldPass = function () { - return $scope.config.hasPassword && - !$scope.config.allowManuallyChangingPassword && - !$scope.config.enablePasswordRetrieval && !$scope.showReset; - }; - - // TODO: I don't think we need this or the cancel button, this can be up to the editor rendering this directive - $scope.showCancelBtn = function () { - return $scope.config.disableToggle !== true && $scope.config.hasPassword; - }; - - } - - function ChangePasswordDirective() { - - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/users/change-password.html', - controller: 'Umbraco.Editors.Users.ChangePasswordDirectiveController', - scope: { - isNew: "=?", - passwordValues: "=", - config: "=" - } - }; - - return directive; - - } - - angular.module('umbraco.directives').controller('Umbraco.Editors.Users.ChangePasswordDirectiveController', ChangePasswordController); - angular.module('umbraco.directives').directive('changePassword', ChangePasswordDirective); + angular.module('umbraco.directives').component('changePassword', component); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 7852180dfa..9cf1181cfa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -104,21 +104,26 @@ function entityResource($q, $http, umbRequestHelper) { * * @param {Int} id Id of node to return the public url to * @param {string} type Object type name + * @param {string} culture Culture * @returns {Promise} resourcePromise object containing the url. * */ - getUrl: function (id, type) { + getUrl: function (id, type, culture) { if (id === -1 || id === "-1") { return ""; } + if (!culture) { + culture = ""; + } + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetUrl", - [{ id: id }, {type: type }])), + [{ id: id }, {type: type }, {culture: culture }])), 'Failed to retrieve url for id:' + id); }, 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 3525310e48..200e61ee96 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 @@ -491,7 +491,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt var savedVariants = []; if (origContent.variants) { isContent = true; - //it's contnet so assign the variants as they exist + //it's content so assign the variants as they exist origVariants = origContent.variants; savedVariants = savedContent.variants; } @@ -517,7 +517,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //special case for content, don't sync this variant if it wasn't tagged //for saving in the first place - if (!origVariant.save) { + if (isContent && !origVariant.save) { continue; } diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index bd1cdd5b4f..b0b7e80762 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -112,7 +112,7 @@ @import "components/umb-editor-navigation-item.less"; @import "components/umb-editor-sub-views.less"; @import "components/editor/subheader/umb-editor-sub-header.less"; -@import "components/umb-flatpickr.less"; +@import "components/umb-date-time-picker.less"; @import "components/umb-grid-selector.less"; @import "components/umb-child-selector.less"; @import "components/umb-group-builder.less"; @@ -124,6 +124,7 @@ @import "components/umb-form-check.less"; @import "components/umb-locked-field.less"; @import "components/umb-tabs.less"; +@import "components/umb-loader.less"; @import "components/umb-load-indicator.less"; @import "components/umb-breadcrumbs.less"; @import "components/umb-media-grid.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 315cd91dbd..33a723a3f7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -1,8 +1,12 @@ -.umb-tour__loader { - background: @white; - z-index: @zindexTourModal; +.umb-loader-wrapper.umb-tour__loader { + margin: 0; position: fixed; - height: 5px; + z-index: @zindexTourModal; + + .umb-loader { + background-color: @white; + height: 5px; + } } .umb-tour__pulse { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index 2a1d047ca1..85fcc249f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -376,14 +376,3 @@ a.umb-variant-switcher__toggle { margin-right: auto; padding-right: 10px; } - -/* Confirm */ -.umb-editor-confirm { - background-color: @white; - padding: 20px; - position: absolute; - left: 0; - bottom: 0; - z-index: 10; - box-shadow: 0 -3px 12px 0px rgba(0,0,0,0.16); -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less index a969f876a4..8945d15ec6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -39,17 +39,10 @@ // Loading Animation // ------------------------ - .l { + .umb-tree-item__loader { width: 100%; - height: 2px; - overflow: hidden; position: absolute; - left: 0; - bottom: -1px; - - div { - .umb-loader; - } + margin: 0; } .umb-tree-item__label { 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 7a70046de2..93850656f0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -33,12 +33,6 @@ display: none; } } - - //loader defaults - .umb-loader { - height: 10px; - margin: 10px 10px 10px 10px; - } .search-subtitle { color: @gray-7; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less similarity index 90% rename from src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less rename to src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less index 8cdcc8b877..2df0cc5fd8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less @@ -1,4 +1,4 @@ -.flatpickr-calendar.flatpickr-calendar { +.flatpickr-calendar { border-radius: @baseBorderRadius; box-shadow: 0 5px 10px 0 rgba(0,0,0,0.16); } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less new file mode 100644 index 0000000000..260710ce72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less @@ -0,0 +1,42 @@ +// Loading Animation +// ------------------------ + +.umb-loader { + background-color: @blue; + margin-top: 0; + margin-left: -100%; + animation-name: bounce_loadingProgressG; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + width: 100%; + height: 2px; +} + +@keyframes bounce_loadingProgressG { + 0% { + margin-left: -100%; + } + + 100% { + margin-left: 100%; + } +} + +.umb-loader-wrapper { + position: absolute; + right: 0; + left: 0; + margin: 10px 0; + overflow: hidden; +} + +.umb-loader-wrapper.-top { + top: 0; + bottom: auto; +} + +.umb-loader-wrapper.-bottom { + top: auto; + bottom: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index f387b6540b..27b64f85fb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -295,7 +295,11 @@ input.umb-table__input { .umb-table__row-expand { font-size: 12px; text-decoration: none; - color: @black; + color: @gray-4; + + &:hover { + color: @black; + } } .umb-table__row-expand--hidden { diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 798252c394..8090b971fc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -13,6 +13,9 @@ @import "../../lib/bootstrap/less/thumbnails.less"; @import "../../lib/bootstrap/less/media.less"; +// Umbraco Components +@import "components/umb-loader.less"; + [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; @@ -165,45 +168,17 @@ input.ng-dirty.ng-invalid{border-color: #b94a48; color: #b94a48;} opacity:1; } +.umb-installer-loader { + margin: 0; + width: 0; + z-index: 777; -.umb-loader{ - background-color: @white; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height: 5px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; + .umb-loader { + background-color: @white; + height: 5px; } } -//loader defaults -.umb-loader-container, .umb-loader-done{ - height: 3px; - position: absolute; - bottom: 0; - left: 0; - overflow: hidden; - width: 0%; - z-index: 777; -} - -.umb-loader-done{ - right: 0%; - background: @white; -} - - .permissions-report { overflow:auto; height:320px; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index c10f846ca5..920fcdb1eb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -486,42 +486,6 @@ table thead a { color:@green; } -// Loading Animation -// ------------------------ - -.umb-loader{ - background-color: @blue; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height:2px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; - } -} - -.umb-loader-wrapper { - position: absolute; - right: 0; - left: 0; - margin: 10px 0; - overflow: hidden; -} - -.umb-loader-wrapper.-bottom { - bottom: 0; -} - // Helpers .strong { diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 92351d09ca..435b64ad34 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -5,7 +5,6 @@ // -------------------------------------------------- .umb-property-editor { width: 100%; - position:relative; } .umb-property-editor-tiny { diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 654bbb1d03..93870f8a56 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -10,7 +10,7 @@ */ function MainController($scope, $location, appState, treeService, notificationsService, userService, historyService, updateChecker, navigationService, eventsService, - tmhDynamicLocale, localStorageService, editorService, overlayService) { + tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { //the null is important because we do an explicit bool check on this in the view $scope.authenticated = null; @@ -21,7 +21,13 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.search = {}; $scope.login = {}; $scope.tabbingActive = false; - + + // Load TinyMCE assets ahead of time in the background for the user + // To help with first load of the RTE + tinyMceAssets.forEach(function (tinyJsAsset) { + assetsService.loadJs(tinyJsAsset, $scope); + }); + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js index 75bf414099..c9d2e43a98 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function CompositionsController($scope, $location, $filter) { + function CompositionsController($scope, $location, $filter, overlayService) { var vm = this; var oldModel = null; @@ -56,19 +56,35 @@ if ($scope.model && $scope.model.submit) { // check if any compositions has been removed - vm.compositionRemoved = false; + var compositionRemoved = false; for (var i = 0; oldModel.compositeContentTypes.length > i; i++) { var oldComposition = oldModel.compositeContentTypes[i]; if (_.contains($scope.model.compositeContentTypes, oldComposition) === false) { - vm.compositionRemoved = true; + compositionRemoved = true; } } /* submit the form if there havne't been removed any composition or the confirm checkbox has been checked */ - if (!vm.compositionRemoved || vm.allowSubmit) { - $scope.model.submit($scope.model); + if (compositionRemoved) { + vm.allowSubmit = false; + const dialog = { + view: "views/common/infiniteeditors/compositions/overlays/confirmremove.html", + submitButtonLabelKey: "general_ok", + closeButtonLabelKey: "general_cancel", + submit: function (model) { + $scope.model.submit($scope.model); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(dialog); + return; } + + $scope.model.submit($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html index bf74431d96..bb0804e2bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html @@ -92,16 +92,6 @@ - -
-
Warning
-

Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?

- -
- diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html new file mode 100644 index 0000000000..dd5e244ba5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/overlays/confirmremove.html @@ -0,0 +1,6 @@ +
+ +
Warning
+

Removing a composition will delete all the associated property data. Once you save the document type there's no way back, are you sure?

+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 5da14fc6d3..b5043293e5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -93,7 +93,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }); } - } else if ($scope.model.target.url.length) { + } else if ($scope.model.target.url && $scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html index 8376f50713..779ca739d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html @@ -109,10 +109,10 @@ - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index bc27196466..676b2efc38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -18,11 +18,11 @@

-
- - + + Required The confirmed password doesn't match the new password! @@ -152,13 +152,13 @@
- - + +
- - + +
Show password @@ -189,8 +189,8 @@
- - + +
@@ -220,13 +220,13 @@
- - + +
- - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index 5410d453c4..9a2fe96289 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -1,6 +1,7 @@
-
+ +
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 f411437b1d..4f7141559c 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 @@ -12,9 +12,10 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html index d777b3db78..af0a3ba097 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-item.html @@ -15,7 +15,8 @@ -
+ +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 026b0637c7..e8e47f6ae6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -14,7 +14,8 @@ icon="icon-merge" action="openCompositionsDialog()" size="xs" - add-ellipsis="true"> + add-ellipsis="true" + state="compositionsButtonState"> +
    +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html index 55d7bd0744..f11df6042e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -60,7 +60,7 @@
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 7b6c7ca0e1..ef5f7a4159 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -1,68 +1,69 @@
-
+
Password has been reset to:
- {{passwordValues.generatedPassword}} + {{vm.passwordValues.generatedPassword}}
-
+
- - - + + - - {{passwordForm.resetPassword.errorMsg}} + ng-change="vm.showReset = !vm.showReset" /> + + {{vm.passwordForm.resetPassword.errorMsg}} - - + + - - + - + Required - {{passwordForm.oldPassword.errorMsg}} + {{vm.passwordForm.oldPassword.errorMsg}} - - + - + Required - Minimum {{config.minPasswordLength}} characters - {{passwordForm.password.errorMsg}} + Minimum {{vm.config.minPasswordLength}} characters + {{vm.passwordForm.password.errorMsg}} - - + - + The confirmed password doesn't match the new password! - + Cancel diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index f906369926..1413965a19 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -51,7 +51,10 @@ function contentCreateController($scope, .search("create", "true") /* when we create a new node we want to make sure it uses the same language as what is selected in the tree */ - .search("cculture", mainCulture); + .search("cculture", mainCulture) + /* when we create a new node we must make sure that any previously + used blueprint is reset */ + .search("blueprintId", null); close(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index 56ce67a0dc..855afc2bd2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -58,7 +58,7 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, $location.path(location); } - navigationService.hideMenu(); + $scope.success = true; }, function(err) { toggleDeleting(false); @@ -74,7 +74,11 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, $scope.cancel = function() { toggleDeleting(false); - navigationService.hideDialog(); + $scope.close(); + }; + + $scope.close = function () { + navigationService.hideDialog(); }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 0ebe577ed8..111dacd7cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -24,9 +24,7 @@ to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index b8303efade..cbedabba79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -42,7 +42,7 @@
    -
  • +
- +
+

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

+ +
+ This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html b/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html index 524bb06354..9ac7ef10fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html @@ -1,9 +1,8 @@
-
-
-
+ +

When items are deleted from the recycle bin, they will be gone forever. diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index fd154f438a..3c073ba404 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -24,9 +24,7 @@ to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index b573acaf52..a0fe7baec2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -15,7 +15,7 @@
-
- + @@ -47,7 +47,7 @@
-
- +
@@ -112,7 +112,7 @@
Publish:  {{variant.releaseDateFormatted}}
- + @@ -138,7 +138,7 @@
Unpublish:  {{variant.expireDateFormatted}}
- Set date
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html index a5cf6b1a41..7e7e70d650 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index 03b090fc54..8b81462ad5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -7,9 +7,7 @@ Select the folder to copy {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html index 46626d924b..fe0bde7f1f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html index 533659e808..300c824329 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html @@ -74,11 +74,11 @@ - - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/delete.html b/src/Umbraco.Web.UI.Client/src/views/media/delete.html index af33574f55..d4f16ce71c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/delete.html @@ -1,10 +1,21 @@
- -

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

- +
+
+ {{currentNode.name}} + was deleted +
+ +
+ +
+

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

+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html b/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html index 9d1b28edc9..33681f9269 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html @@ -2,9 +2,8 @@
-
-
-
+ +

When items are deleted from the recycle bin, they will be gone forever. diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index ab057f87a4..8792571377 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -50,8 +50,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer $location.path(location); } - navigationService.hideMenu(); - + $scope.success = true; }, function (err) { $scope.currentNode.loading = false; @@ -66,7 +65,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer }); }; - $scope.cancel = function() { + $scope.close = function() { navigationService.hideDialog(); }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html index 5e7b9c6669..9c21f623b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html @@ -7,9 +7,7 @@ Select the folder to copy {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html index 0e02c7ad0e..5225a41a0d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/member/create.html b/src/Umbraco.Web.UI.Client/src/views/member/create.html index ff7e954c02..5f4ad77f04 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/create.html @@ -6,7 +6,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 0cf67022c6..db0e9a016e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -1,7 +1,7 @@ 
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 52b105201e..48170fb4b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RTEController", - function ($scope, $q, assetsService, $timeout, tinyMceService, angularHelper) { + function ($scope, $q, assetsService, $timeout, tinyMceService, angularHelper, tinyMceAssets) { // TODO: A lot of the code below should be shared between the grid rte and the normal rte @@ -30,9 +30,9 @@ angular.module("umbraco") var promises = []; //queue file loading - if (typeof tinymce === "undefined") { // Don't reload tinymce if already loaded - promises.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope)); - } + tinyMceAssets.forEach(function (tinyJsAsset) { + promises.push(assetsService.loadJs(tinyJsAsset, $scope)); + }); //stores a reference to the editor var tinyMceEditor = null; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 9c476103a5..1e51d4585e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -2,7 +2,7 @@ "use strict"; function UserGroupsController($scope, $timeout, $location, $filter, userService, userGroupsResource, - formHelper, localizationService, listViewHelper) { + formHelper, localizationService, listViewHelper, overlayService) { var vm = this; @@ -95,18 +95,26 @@ if(vm.selection.length > 0) { - localizationService.localize("defaultdialogs_confirmdelete") - .then(function(value) { - - var confirmResponse = confirm(value); - - if (confirmResponse === true) { - userGroupsResource.deleteUserGroups(_.pluck(vm.selection, "id")).then(function (data) { - clearSelection(); - onInit(); - }, angular.noop); - } - + localizationService.localizeMany(["general_delete", "defaultdialogs_confirmdelete", "general_cancel", "contentTypeEditor_yesDelete"]) + .then(function (data) { + const overlay = { + title: data[0], + content: data[1] + "?", + closeButtonLabel: data[2], + submitButtonLabel: data[3], + submitButtonStyle: "danger", + close: function () { + overlayService.close(); + }, + submit: function () { + userGroupsResource.deleteUserGroups(_.pluck(vm.selection, "id")).then(function (data) { + clearSelection(); + onInit(); + }, angular.noop); + overlayService.close(); + } + }; + overlayService.open(overlay); }); } diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js index 9668a03580..274d256a64 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/rte-controller.spec.js @@ -1,7 +1,9 @@ describe('RTE controller tests', function () { var scope, controllerFactory; - beforeEach(module('umbraco')); + beforeEach(module('umbraco', function ($provide) { + $provide.value('tinyMceAssets', []); + })); beforeEach(inject(function ($rootScope, $controller) { controllerFactory = $controller; @@ -20,4 +22,5 @@ describe('RTE controller tests', function () { }); }); -}); \ No newline at end of file +}); + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c3d913be4c..3517024d0c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -347,10 +347,10 @@ True 8200 / - http://localhost:8200 - 8120 + http://localhost:8200/ + 8130 / - http://localhost:8120 + http://localhost:8130 False False diff --git a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml index e74dbea217..6941fe0367 100644 --- a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml @@ -19,11 +19,13 @@ -
-
-
+ + -
+
til i træstrukturen nedenfor blev flyttet til blev kopieret til + blev slettet Rettigheder Fortryd ændringer Send til udgivelse diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 2e40958e92..a8114faf93 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -38,6 +38,7 @@ to in the tree structure below was moved to was copied to + was deleted Permissions Rollback Send To Publish diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 043ca2fb7e..a933b8507d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -38,6 +38,7 @@ to in the tree structure below was moved to was copied to + was deleted Permissions Rollback Send To Publish diff --git a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js index 654bbb1d03..93870f8a56 100644 --- a/src/Umbraco.Web.UI/Umbraco/js/main.controller.js +++ b/src/Umbraco.Web.UI/Umbraco/js/main.controller.js @@ -10,7 +10,7 @@ */ function MainController($scope, $location, appState, treeService, notificationsService, userService, historyService, updateChecker, navigationService, eventsService, - tmhDynamicLocale, localStorageService, editorService, overlayService) { + tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { //the null is important because we do an explicit bool check on this in the view $scope.authenticated = null; @@ -21,7 +21,13 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.search = {}; $scope.login = {}; $scope.tabbingActive = false; - + + // Load TinyMCE assets ahead of time in the background for the user + // To help with first load of the RTE + tinyMceAssets.forEach(function (tinyJsAsset) { + assetsService.loadJs(tinyJsAsset, $scope); + }); + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { diff --git a/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config b/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config index 40630b7eb6..f6a26ee89a 100644 --- a/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config +++ b/src/Umbraco.Web.UI/config/tinyMceConfig.Release.config @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + diff --git a/src/Umbraco.Web.UI/config/tinyMceConfig.config b/src/Umbraco.Web.UI/config/tinyMceConfig.config index a686021cbf..7f7cb657e6 100644 --- a/src/Umbraco.Web.UI/config/tinyMceConfig.config +++ b/src/Umbraco.Web.UI/config/tinyMceConfig.config @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@ - + diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 4e420206d3..9d5af028e3 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2297,7 +2297,7 @@ namespace Umbraco.Web.Editors } var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) + if (entry == null || entry.ProtectedNodeId != content.Id) { return Request.CreateResponse(HttpStatusCode.OK); } @@ -2379,7 +2379,7 @@ namespace Umbraco.Web.Editors var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) + if (entry == null || entry.ProtectedNodeId != content.Id) { entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 5e094534d2..0513017b70 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -211,17 +211,20 @@ namespace Umbraco.Web.Editors ///
/// Int id of the entity to fetch URL for /// The type of entity such as Document, Media, Member + /// The culture to fetch the URL for /// The URL or path to the item /// /// We are not restricting this with security because there is no sensitive data /// - public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) + public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type, string culture = null) { + culture = culture ?? ClientCulture(); + var returnUrl = string.Empty; if (type == UmbracoEntityTypes.Document) { - var foundUrl = UmbracoContext.Url(id); + var foundUrl = UmbracoContext.Url(id, culture); if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") { returnUrl = foundUrl; diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index 26a1d97f67..1d518fa1d3 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -13,6 +13,7 @@ using Umbraco.Web.Models; namespace Umbraco.Web { using Core.Configuration; + using Umbraco.Web.JavaScript; /// /// HtmlHelper extensions for the back office @@ -118,6 +119,21 @@ namespace Umbraco.Web sb.AppendLine(JsonConvert.SerializeObject(resetCodeModel)); sb.AppendLine(@"});"); + return html.Raw(sb.ToString()); + } + + public static IHtmlString AngularValueTinyMceAssets(this HtmlHelper html) + { + var ctx = new HttpContextWrapper(HttpContext.Current); + var files = JsInitialization.OptimizeTinyMceScriptFiles(ctx); + + var sb = new StringBuilder(); + + sb.AppendLine(@"app.value(""tinyMceAssets"","); + sb.AppendLine(JsonConvert.SerializeObject(files)); + sb.AppendLine(@");"); + + return html.Raw(sb.ToString()); } } diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 6cf5092bb0..4606a58a3a 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -19,10 +19,26 @@ namespace Umbraco.Web /// Replaces text line breaks with HTML line breaks /// /// The text. - /// The text with text line breaks replaced with HTML line breaks (
)
+ /// The text with text line breaks replaced with HTML line breaks (<br />). + [Obsolete("This method doesn't HTML encode the text. Use ReplaceLineBreaks instead.")] public HtmlString ReplaceLineBreaksForHtml(string text) { - return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + } + + /// + /// HTML encodes the text and replaces text line breaks with HTML line breaks. + /// + /// The text. + /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). + public IHtmlString ReplaceLineBreaks(string text) + { + var value = HttpUtility.HtmlEncode(text)? + .Replace("\r\n", "
") + .Replace("\r", "
") + .Replace("\n", "
"); + + return new HtmlString(value); } public HtmlString StripHtmlTags(string html, params string[] tags) diff --git a/src/Umbraco.Web/JavaScript/JsInitialization.cs b/src/Umbraco.Web/JavaScript/JsInitialization.cs index 638b1bf5b4..17e4f7b094 100644 --- a/src/Umbraco.Web/JavaScript/JsInitialization.cs +++ b/src/Umbraco.Web/JavaScript/JsInitialization.cs @@ -131,6 +131,17 @@ namespace Umbraco.Web.JavaScript return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()); } + internal static IEnumerable GetTinyMceInitialization() + { + var resources = JsonConvert.DeserializeObject(Resources.TinyMceInitialize); + return resources.Where(x => x.Type == JTokenType.String).Select(x => x.ToString()); + } + + internal static IEnumerable OptimizeTinyMceScriptFiles(HttpContextBase httpContext) + { + return OptimizeScriptFiles(httpContext, GetTinyMceInitialization()); + } + /// /// Parses the JsResources.Main and replaces the replacement tokens accordingly. /// diff --git a/src/Umbraco.Web/JavaScript/Resources.Designer.cs b/src/Umbraco.Web/JavaScript/Resources.Designer.cs index ca571b1186..f11839f6ca 100644 --- a/src/Umbraco.Web/JavaScript/Resources.Designer.cs +++ b/src/Umbraco.Web/JavaScript/Resources.Designer.cs @@ -146,5 +146,17 @@ namespace Umbraco.Web.JavaScript { return ResourceManager.GetString("ServerVariables", resourceCulture); } } + + /// + /// Looks up a localized string similar to [ + /// '../lib/tinymce/tinymce.min.js', + ///] + ///. + /// + internal static string TinyMceInitialize { + get { + return ResourceManager.GetString("TinyMceInitialize", resourceCulture); + } + } } } diff --git a/src/Umbraco.Web/JavaScript/Resources.resx b/src/Umbraco.Web/JavaScript/Resources.resx index 95526f51e7..1adcaeeb4a 100644 --- a/src/Umbraco.Web/JavaScript/Resources.resx +++ b/src/Umbraco.Web/JavaScript/Resources.resx @@ -130,4 +130,7 @@ servervariables.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - + + TinyMceInitialize.js;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + \ No newline at end of file diff --git a/src/Umbraco.Web/JavaScript/TinyMceInitialize.js b/src/Umbraco.Web/JavaScript/TinyMceInitialize.js new file mode 100644 index 0000000000..6cb9cf6277 --- /dev/null +++ b/src/Umbraco.Web/JavaScript/TinyMceInitialize.js @@ -0,0 +1,17 @@ +[ + 'lib/tinymce/tinymce.min.js', + + 'lib/tinymce/plugins/paste/plugin.min.js', + 'lib/tinymce/plugins/anchor/plugin.min.js', + 'lib/tinymce/plugins/charmap/plugin.min.js', + 'lib/tinymce/plugins/table/plugin.min.js', + 'lib/tinymce/plugins/lists/plugin.min.js', + 'lib/tinymce/plugins/advlist/plugin.min.js', + 'lib/tinymce/plugins/hr/plugin.min.js', + 'lib/tinymce/plugins/autolink/plugin.min.js', + 'lib/tinymce/plugins/directionality/plugin.min.js', + 'lib/tinymce/plugins/tabfocus/plugin.min.js', + 'lib/tinymce/plugins/searchreplace/plugin.min.js', + 'lib/tinymce/plugins/fullscreen/plugin.min.js', + 'lib/tinymce/plugins/noneditable/plugin.min.js' +] diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index 38ec557fdb..eb474e4cbe 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -46,10 +46,19 @@ namespace Umbraco.Web.Models.Mapping target.Icon = Constants.Icons.Member; if (source is IContentEntitySlim contentSlim) + { source.AdditionalData["ContentTypeAlias"] = contentSlim.ContentTypeAlias; + } + + if (source is IDocumentEntitySlim documentSlim) + { + source.AdditionalData["IsPublished"] = documentSlim.Published; + } if (source is IMediaEntitySlim mediaSlim) + { source.AdditionalData["MediaPath"] = mediaSlim.MediaPath; + } // NOTE: we're mapping the objects in AdditionalData by object reference here. // it works fine for now, but it's something to keep in mind in the future diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs index 085d66f3c6..0cf36122d7 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs @@ -41,9 +41,7 @@ namespace Umbraco.Web.PropertyEditors { var controlVal = control.Value; - // TODO: If it's not a string, then it's a json formatted value - - // we cannot really index this in a smart way since it could be 'anything' - if (controlVal.Type == JTokenType.String) + if (controlVal?.Type == JTokenType.String) { var str = controlVal.Value(); str = XmlHelper.CouldItBeXml(str) ? str.StripHtml() : str; @@ -53,14 +51,22 @@ namespace Umbraco.Web.PropertyEditors //add the row name as an individual field result.Add(new KeyValuePair>($"{property.Alias}.{rowName}", new[] { str })); } + else if (controlVal is JContainer jc) + { + foreach (var s in jc.Descendants().Where(t => t.Type == JTokenType.String)) + { + sb.Append(s.Value()); + sb.Append(" "); + } + } } } + //First save the raw value to a raw field + result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); + if (sb.Length > 0) { - //First save the raw value to a raw field - result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); - //index the property with the combined/cleaned value result.Add(new KeyValuePair>(property.Alias, new[] { sb.ToString() })); } diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs new file mode 100644 index 0000000000..1208a5eecc --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleMediaPickerParameterEditor.cs @@ -0,0 +1,27 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ParameterEditors +{ + /// + /// Represents a multiple media picker macro parameter editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.MultipleMediaPicker, + EditorType.MacroParameter, + "Multiple Media Picker", + "mediapicker", + ValueType = ValueTypes.Text)] + public class MultipleMediaPickerParameterEditor : DataEditor + { + /// + /// Initializes a new instance of the class. + /// + public MultipleMediaPickerParameterEditor(ILogger logger) + : base(logger) + { + DefaultConfiguration.Add("multiPicker", "1"); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index d2272a25b5..a56630d7c5 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -37,14 +37,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { var isMultiple = IsMultipleDataType(propertyType.DataType); - var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); - return isMultiple - ? isOnlyImages - ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(ImageTypeAlias)) - : typeof(IEnumerable) - : isOnlyImages - ? ModelType.For(ImageTypeAlias) + ? typeof(IEnumerable) : typeof(IPublishedContent); } @@ -57,12 +51,6 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.Multiple; } - private bool IsOnlyImagesDataType(PublishedDataType dataType) - { - var config = ConfigurationEditor.ConfigurationAs(dataType.Configuration); - return config.OnlyImages; - } - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) { @@ -79,12 +67,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters PropertyCacheLevel cacheLevel, object source, bool preview) { var isMultiple = IsMultipleDataType(propertyType.DataType); - var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); var udis = (Udi[]) source; - var mediaItems = isOnlyImages - ? _publishedModelFactory.CreateModelList(ImageTypeAlias) - : new List(); + var mediaItems = new List(); if (source == null) return isMultiple ? mediaItems : null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index cb6ce6dd6d..d5e1f841ea 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web.Macros; +using System.Web; namespace Umbraco.Web.PropertyEditors.ValueConverters { @@ -63,7 +64,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } } - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + var converted = Convert(inter, preview); + + return new HtmlString(converted == null ? string.Empty : converted); + } + + private string Convert(object source, bool preview) { if (source == null) { diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d7283a1e90..d15d5817f6 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1067,7 +1067,7 @@ namespace Umbraco.Web /// /// Note that in V7 this method also return the content node self. /// - public static IEnumerable Siblings(this IPublishedContent content, string culture = null) + public static IEnumerable Siblings(this IPublishedContent content, string culture = null) where T : class, IPublishedContent { return SiblingsAndSelf(content, culture).Where(x => x.Id != content.Id); diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index 6177d9b868..2ba78b2080 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -1,39 +1,18 @@ using System; -using System.Linq; -using System.Web.Helpers; using System.Web.Http; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Web.Install; -using Umbraco.Web.Mvc; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi; namespace Umbraco.Web.Runtime { public class WebFinalComponent : IComponent { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly SurfaceControllerTypeCollection _surfaceControllerTypes; - private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; - private readonly IGlobalSettings _globalSettings; - public WebFinalComponent(IUmbracoContextAccessor umbracoContextAccessor, SurfaceControllerTypeCollection surfaceControllerTypes, UmbracoApiControllerTypeCollection apiControllerTypes, IGlobalSettings globalSettings) + public WebFinalComponent() { - _umbracoContextAccessor = umbracoContextAccessor; - _surfaceControllerTypes = surfaceControllerTypes; - _apiControllerTypes = apiControllerTypes; - _globalSettings = globalSettings; } public void Initialize() { - // set routes - CreateRoutes(_umbracoContextAccessor, _globalSettings, _surfaceControllerTypes, _apiControllerTypes); - // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); } @@ -41,92 +20,5 @@ namespace Umbraco.Web.Runtime public void Terminate() { } - // internal for tests - internal static void CreateRoutes( - IUmbracoContextAccessor umbracoContextAccessor, - IGlobalSettings globalSettings, - SurfaceControllerTypeCollection surfaceControllerTypes, - UmbracoApiControllerTypeCollection apiControllerTypes) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(); - - // create the front-end route - var defaultRoute = RouteTable.Routes.MapRoute( - "Umbraco_default", - umbracoPath + "/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } - ); - defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory()); - - // register install routes - RouteTable.Routes.RegisterArea(); - - // register all back office routes - RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings)); - - // plugin controllers must come first because the next route will catch many things - RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes); - } - - private static void RoutePluginControllers( - IGlobalSettings globalSettings, - SurfaceControllerTypeCollection surfaceControllerTypes, - UmbracoApiControllerTypeCollection apiControllerTypes) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(); - - // need to find the plugin controllers and route them - var pluginControllers = surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); - - // local controllers do not contain the attribute - var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); - foreach (var s in localControllers) - { - if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalSurfaceController(s, umbracoPath); - else if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalApiController(s, umbracoPath); - } - - // get the plugin controllers that are unique to each area (group by) - var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); - var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); - // loop through each area defined amongst the controllers - foreach (var g in groupedAreas) - { - // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) - var pluginControllerArea = new PluginControllerArea(globalSettings, g.Select(PluginController.GetMetadata)); - RouteTable.Routes.RegisterArea(pluginControllerArea); - } - } - - private static void RouteLocalApiController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapHttpRoute( - $"umbraco-api-{meta.ControllerName}", - url, // url to match - new { controller = meta.ControllerName, id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); - if (route.DataTokens == null) // web api routes don't set the data tokens object - route.DataTokens = new RouteValueDictionary(); - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set - } - - private static void RouteLocalSurfaceController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapRoute( - $"umbraco-surface-{meta.ControllerName}", - url, // url to match - new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); // look in this namespace to create the controller - route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set - route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! - // make it use our custom/special SurfaceMvcHandler - route.RouteHandler = new SurfaceRouteHandler(); - } } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs index e1e0f4d80a..ac813d7196 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComponent.cs @@ -8,12 +8,14 @@ using System.Web.Configuration; using System.Web.Http; using System.Web.Http.Dispatcher; using System.Web.Mvc; +using System.Web.Routing; using ClientDependency.Core.CompositeFiles.Providers; using ClientDependency.Core.Config; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Web.Install; using Umbraco.Web.JavaScript; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -24,10 +26,16 @@ namespace Umbraco.Web.Runtime { public sealed class WebInitialComponent : IComponent { + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly SurfaceControllerTypeCollection _surfaceControllerTypes; + private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; private readonly IGlobalSettings _globalSettings; - public WebInitialComponent(IGlobalSettings globalSettings) + public WebInitialComponent(IUmbracoContextAccessor umbracoContextAccessor, SurfaceControllerTypeCollection surfaceControllerTypes, UmbracoApiControllerTypeCollection apiControllerTypes, IGlobalSettings globalSettings) { + _umbracoContextAccessor = umbracoContextAccessor; + _surfaceControllerTypes = surfaceControllerTypes; + _apiControllerTypes = apiControllerTypes; _globalSettings = globalSettings; } @@ -51,6 +59,9 @@ namespace Umbraco.Web.Runtime // add global filters ConfigureGlobalFilters(); + + // set routes + CreateRoutes(_umbracoContextAccessor, _globalSettings, _surfaceControllerTypes, _apiControllerTypes); } public void Terminate() @@ -131,5 +142,94 @@ namespace Umbraco.Web.Runtime ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); } + + // internal for tests + internal static void CreateRoutes( + IUmbracoContextAccessor umbracoContextAccessor, + IGlobalSettings globalSettings, + SurfaceControllerTypeCollection surfaceControllerTypes, + UmbracoApiControllerTypeCollection apiControllerTypes) + { + var umbracoPath = globalSettings.GetUmbracoMvcArea(); + + // create the front-end route + var defaultRoute = RouteTable.Routes.MapRoute( + "Umbraco_default", + umbracoPath + "/RenderMvc/{action}/{id}", + new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } + ); + defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory()); + + // register install routes + RouteTable.Routes.RegisterArea(); + + // register all back office routes + RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings)); + + // plugin controllers must come first because the next route will catch many things + RoutePluginControllers(globalSettings, surfaceControllerTypes, apiControllerTypes); + } + + private static void RoutePluginControllers( + IGlobalSettings globalSettings, + SurfaceControllerTypeCollection surfaceControllerTypes, + UmbracoApiControllerTypeCollection apiControllerTypes) + { + var umbracoPath = globalSettings.GetUmbracoMvcArea(); + + // need to find the plugin controllers and route them + var pluginControllers = surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); + + // local controllers do not contain the attribute + var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); + foreach (var s in localControllers) + { + if (TypeHelper.IsTypeAssignableFrom(s)) + RouteLocalSurfaceController(s, umbracoPath); + else if (TypeHelper.IsTypeAssignableFrom(s)) + RouteLocalApiController(s, umbracoPath); + } + + // get the plugin controllers that are unique to each area (group by) + var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); + var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); + // loop through each area defined amongst the controllers + foreach (var g in groupedAreas) + { + // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) + var pluginControllerArea = new PluginControllerArea(globalSettings, g.Select(PluginController.GetMetadata)); + RouteTable.Routes.RegisterArea(pluginControllerArea); + } + } + + private static void RouteLocalApiController(Type controller, string umbracoPath) + { + var meta = PluginController.GetMetadata(controller); + var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; + var route = RouteTable.Routes.MapHttpRoute( + $"umbraco-api-{meta.ControllerName}", + url, // url to match + new { controller = meta.ControllerName, id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); + if (route.DataTokens == null) // web api routes don't set the data tokens object + route.DataTokens = new RouteValueDictionary(); + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set + } + + private static void RouteLocalSurfaceController(Type controller, string umbracoPath) + { + var meta = PluginController.GetMetadata(controller); + var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; + var route = RouteTable.Routes.MapRoute( + $"umbraco-surface-{meta.ControllerName}", + url, // url to match + new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); // look in this namespace to create the controller + route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set + route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! + // make it use our custom/special SurfaceMvcHandler + route.RouteHandler = new SurfaceRouteHandler(); + } + } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 729b2c0a9d..c3c3dc75ce 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -384,19 +384,7 @@ namespace Umbraco.Web.Search /// /// private IEnumerable MediaFromSearchResults(IEnumerable results) - { - //add additional data - foreach (var result in results) - { - var m = _mapper.Map(result); - //if no icon could be mapped, it will be set to document, so change it to picture - if (m.Icon == Constants.Icons.DefaultIcon) - { - m.Icon = Constants.Icons.MediaImage; - } - yield return m; - } - } + => _mapper.Map>(results); /// /// Returns a collection of entities for content based on search results diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dc06222209..222d58d4dc 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -224,6 +224,7 @@ + @@ -1205,6 +1206,7 @@ +