diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj index 6b0f92858d..99e72bae0f 100644 --- a/build/templates/UmbracoProject/UmbracoProject.csproj +++ b/build/templates/UmbracoProject/UmbracoProject.csproj @@ -8,6 +8,12 @@ $(DefaultItemExcludes);wwwroot/media/**; + + + + + + @@ -30,6 +36,10 @@ + + true + + false diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 4045421eb1..1b7c6d46fc 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -47,6 +47,7 @@ namespace JsonSchema public RichTextEditorSettings RichTextEditor { get; set; } public RuntimeMinificationSettings RuntimeMinification { get; set; } public BasicAuthSettings BasicAuth { get; set; } + public PackageMigrationSettings PackageMigration { get; set; } } /// diff --git a/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs b/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs new file mode 100644 index 0000000000..27968fdcd2 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/PackageMigrationSettings.cs @@ -0,0 +1,40 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + /// + /// Typed configuration options for package migration settings. + /// + [UmbracoOptions(Constants.Configuration.ConfigPackageMigration)] + public class PackageMigrationSettings + { + private const bool StaticRunSchemaAndContentMigrations = true; + private const bool StaticAllowComponentOverrideOfRunSchemaAndContentMigrations = true; + + /// + /// Gets or sets a value indicating whether package migration steps that install schema and content should run. + /// + /// + /// By default this is true and schema and content defined in a package migration are installed. + /// Using configuration, administrators can optionally switch this off in certain environments. + /// Deployment tools such as Umbraco Deploy can also configure this option to run or not run these migration + /// steps as is appropriate for normal use of the tool. + /// + [DefaultValue(StaticRunSchemaAndContentMigrations)] + public bool RunSchemaAndContentMigrations { get; set; } = StaticRunSchemaAndContentMigrations; + + /// + /// Gets or sets a value indicating whether components can override the configured value for . + /// + /// + /// By default this is true and components can override the configured setting for . + /// If an administrator wants explicit control over which environments migration steps installing schema and content can run, + /// they can set this to false. Components should respect this and not override the configuration. + /// + [DefaultValue(StaticAllowComponentOverrideOfRunSchemaAndContentMigrations)] + public bool AllowComponentOverrideOfRunSchemaAndContentMigrations { get; set; } = StaticAllowComponentOverrideOfRunSchemaAndContentMigrations; + } +} diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs index b03528fd0a..fe999f7bc0 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMinificationSettings.cs @@ -7,6 +7,7 @@ namespace Umbraco.Cms.Core.Configuration.Models { internal const bool StaticUseInMemoryCache = false; internal const string StaticCacheBuster = "Version"; + internal const string StaticVersion = null; /// /// Use in memory cache @@ -19,5 +20,11 @@ namespace Umbraco.Cms.Core.Configuration.Models /// [DefaultValue(StaticCacheBuster)] public RuntimeMinificationCacheBuster CacheBuster { get; set; } = Enum.Parse(StaticCacheBuster); + + /// + /// The unique version string used if CacheBuster is 'Version'. + /// + [DefaultValue(StaticVersion)] + public string Version { get; set; } = StaticVersion; } } diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs index 08020f6e89..7103a9534e 100644 --- a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -1,17 +1,19 @@ -using System.ComponentModel; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Umbraco.Cms.Core.Configuration.Models { - /// /// Typed configuration options for unattended settings. /// [UmbracoOptions(Constants.Configuration.ConfigUnattended)] public class UnattendedSettings { - internal const bool StaticInstallUnattended = false; - internal const bool StaticUpgradeUnattended = false; + private const bool StaticInstallUnattended = false; + private const bool StaticUpgradeUnattended = false; /// /// Gets or sets a value indicating whether unattended installs are enabled. diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 0c7657d07e..c36f5813ab 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -52,6 +52,7 @@ public const string ConfigWebRouting = ConfigPrefix + "WebRouting"; public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; + public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; } } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/StaticServiceProvider.cs b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs similarity index 64% rename from src/Umbraco.Web.Common/DependencyInjection/StaticServiceProvider.cs rename to src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs index c73685b41d..8d195c56f4 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/StaticServiceProvider.cs +++ b/src/Umbraco.Core/DependencyInjection/StaticServiceProvider.cs @@ -4,22 +4,22 @@ using System.ComponentModel; namespace Umbraco.Cms.Web.Common.DependencyInjection { /// - /// INTERNAL Service locator. Should only be used if no other ways exist. + /// Service locator for internal (umbraco cms) only purposes. Should only be used if no other ways exist. /// /// /// It is created with only two goals in mind /// 1) Continue to have the same extension methods on IPublishedContent and IPublishedElement as in V8. To make migration easier. - /// 2) To have a tool to avoid breaking changes in minor versions. All methods using this should in theory be obsolete. + /// 2) To have a tool to avoid breaking changes in minor and patch versions. All methods using this should in theory be obsolete. /// /// Keep in mind, every time this is used, the code becomes basically untestable. /// [EditorBrowsable(EditorBrowsableState.Never)] - internal static class StaticServiceProvider + public static class StaticServiceProvider { /// /// The service locator. /// [EditorBrowsable(EditorBrowsableState.Never)] - internal static IServiceProvider Instance { get; set; } + public static IServiceProvider Instance { get; set; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 77902cc5c1..9b31ed7056 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -72,7 +72,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); return builder; } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 6f6a53df66..5aa62eae19 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -38,6 +38,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection @@ -179,6 +180,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); // will be injected in controllers when needed to invoke rest endpoints on Our Services.AddUnique(); diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index 8385de5e70..daca62926a 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -3,15 +3,15 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Xml.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -165,7 +165,15 @@ namespace Umbraco.Extensions return ContentStatus.Unpublished; } - + /// + /// Gets a collection containing the ids of all ancestors. + /// + /// to retrieve ancestors for + /// An Enumerable list of integer ids + public static IEnumerable GetAncestorIds(this IContent content) => + content.Path.Split(Constants.CharArrays.Comma) + .Where(x => x != Constants.System.RootString && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(s => + int.Parse(s, CultureInfo.InvariantCulture)); #endregion diff --git a/src/Umbraco.Core/Models/UserData.cs b/src/Umbraco.Core/Models/UserData.cs new file mode 100644 index 0000000000..07b45b3c54 --- /dev/null +++ b/src/Umbraco.Core/Models/UserData.cs @@ -0,0 +1,19 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class UserData + { + [DataMember(Name = "name")] + public string Name { get; } + [DataMember(Name = "data")] + public string Data { get; } + + public UserData(string name, string data) + { + Name = name; + Data = data; + } + } +} diff --git a/src/Umbraco.Core/Packaging/InstallationSummary.cs b/src/Umbraco.Core/Packaging/InstallationSummary.cs index 2aa74474d1..42ac9f7ef0 100644 --- a/src/Umbraco.Core/Packaging/InstallationSummary.cs +++ b/src/Umbraco.Core/Packaging/InstallationSummary.cs @@ -32,6 +32,7 @@ namespace Umbraco.Cms.Core.Packaging public IEnumerable PartialViewsInstalled { get; set; } = Enumerable.Empty(); public IEnumerable ContentInstalled { get; set; } = Enumerable.Empty(); public IEnumerable MediaInstalled { get; set; } = Enumerable.Empty(); + public IEnumerable EntityContainersInstalled { get; set; } = Enumerable.Empty(); public override string ToString() { @@ -77,6 +78,7 @@ namespace Umbraco.Cms.Core.Packaging WriteCount("Stylesheets installed: ", StylesheetsInstalled); WriteCount("Scripts installed: ", ScriptsInstalled); WriteCount("Partial views installed: ", PartialViewsInstalled); + WriteCount("Entity containers installed: ", EntityContainersInstalled); WriteCount("Content items installed: ", ContentInstalled); WriteCount("Media items installed: ", MediaInstalled, false); diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index f069adffeb..59896a8e43 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -253,6 +253,11 @@ namespace Umbraco.Cms.Core.Routing builder.SetPublishedContent(content); } + if (!builder.HasDomain()) + { + FindDomain(builder); + } + return BuildRequest(builder); } diff --git a/src/Umbraco.Core/Services/IUserDataService.cs b/src/Umbraco.Core/Services/IUserDataService.cs new file mode 100644 index 0000000000..e63ee3f697 --- /dev/null +++ b/src/Umbraco.Core/Services/IUserDataService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface IUserDataService + { + IEnumerable GetUserData(); + } +} diff --git a/src/Umbraco.Core/Services/UserDataService.cs b/src/Umbraco.Core/Services/UserDataService.cs new file mode 100644 index 0000000000..490b5af6a8 --- /dev/null +++ b/src/Umbraco.Core/Services/UserDataService.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services +{ + public class UserDataService : IUserDataService + { + private readonly IUmbracoVersion _version; + private readonly ILocalizationService _localizationService; + + public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) + { + _version = version; + _localizationService = localizationService; + } + + public IEnumerable GetUserData() => + new List + { + new("Server OS", RuntimeInformation.OSDescription), + new("Server Framework", RuntimeInformation.FrameworkDescription), + new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), + new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), + new("Current Culture", Thread.CurrentThread.CurrentCulture.ToString()), + new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), + new("Current Webserver", GetCurrentWebServer()) + }; + + private string GetCurrentWebServer() => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; + + public bool IsRunningInProcessIIS() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + string processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); + return (processName.Contains("w3wp") || processName.Contains("iisexpress")); + } + } +} diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index b861bcc68b..208ac536c1 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel; using System.Linq; @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Core public int CompareTo(Udi other) { - return string.Compare(UriValue.ToString(), other.UriValue.ToString(), StringComparison.InvariantCultureIgnoreCase); + return string.Compare(UriValue.ToString(), other.UriValue.ToString(), StringComparison.OrdinalIgnoreCase); } public override string ToString() diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs index c14d3e5119..fef61a54c3 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilder.cs @@ -1,8 +1,9 @@ using System; using System.Xml.Linq; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; -using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; @@ -20,7 +21,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context) + IMigrationContext context, + IOptions options) : base(new ImportPackageBuilderExpression( packagingService, mediaService, @@ -28,7 +30,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging mediaUrlGenerators, shortStringHelper, contentTypeBaseServiceProvider, - context)) + context, + options)) { } diff --git a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs index 8eda0f0b45..838d59e14e 100644 --- a/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs +++ b/src/Umbraco.Infrastructure/Packaging/ImportPackageBuilderExpression.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Xml.Linq; using System.Xml.XPath; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Packaging; @@ -25,7 +27,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; private readonly IPackagingService _packagingService; private readonly IShortStringHelper _shortStringHelper; - private bool _executed; + private readonly PackageMigrationSettings _packageMigrationSettings; + + private bool _executed; public ImportPackageBuilderExpression( IPackagingService packagingService, @@ -34,7 +38,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context) : base(context) + IMigrationContext context, + IOptions packageMigrationSettings) : base(context) { _packagingService = packagingService; _mediaService = mediaService; @@ -42,6 +47,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging _mediaUrlGenerators = mediaUrlGenerators; _shortStringHelper = shortStringHelper; _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationSettings = packageMigrationSettings.Value; } /// @@ -59,6 +65,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging } _executed = true; + Context.BuildingExpression = false; if (EmbeddedResourceMigrationType == null && PackageDataManifest == null) @@ -67,6 +74,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging $"Nothing to execute, neither {nameof(EmbeddedResourceMigrationType)} or {nameof(PackageDataManifest)} has been set."); } + if (!_packageMigrationSettings.RunSchemaAndContentMigrations) + { + Logger.LogInformation("Skipping import of embedded schema file, due to configuration"); + return; + } + InstallationSummary installationSummary; if (EmbeddedResourceMigrationType != null) { diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index c691b74a0c..5c9942f945 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -91,19 +91,25 @@ namespace Umbraco.Cms.Infrastructure.Packaging var installationSummary = new InstallationSummary(compiledPackage.Name) { Warnings = compiledPackage.Warnings, - DataTypesInstalled = ImportDataTypes(compiledPackage.DataTypes.ToList(), userId), + DataTypesInstalled = ImportDataTypes(compiledPackage.DataTypes.ToList(), userId, out IEnumerable dataTypeEntityContainersInstalled), LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId), DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId), MacrosInstalled = ImportMacros(compiledPackage.Macros, userId), MacroPartialViewsInstalled = ImportMacroPartialViews(compiledPackage.MacroPartialViews, userId), TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId), - DocumentTypesInstalled = ImportDocumentTypes(compiledPackage.DocumentTypes, userId), - MediaTypesInstalled = ImportMediaTypes(compiledPackage.MediaTypes, userId), + DocumentTypesInstalled = ImportDocumentTypes(compiledPackage.DocumentTypes, userId, out IEnumerable documentTypeEntityContainersInstalled), + MediaTypesInstalled = ImportMediaTypes(compiledPackage.MediaTypes, userId, out IEnumerable mediaTypeEntityContainersInstalled), StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId), ScriptsInstalled = ImportScripts(compiledPackage.Scripts, userId), PartialViewsInstalled = ImportPartialViews(compiledPackage.PartialViews, userId) }; + var entityContainersInstalled = new List(); + entityContainersInstalled.AddRange(dataTypeEntityContainersInstalled); + entityContainersInstalled.AddRange(documentTypeEntityContainersInstalled); + entityContainersInstalled.AddRange(mediaTypeEntityContainersInstalled); + installationSummary.EntityContainersInstalled = entityContainersInstalled; + // We need a reference to the imported doc types to continue var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x); var importedMediaTypes = installationSummary.MediaTypesInstalled.ToDictionary(x => x.Alias, x => x); @@ -116,6 +122,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging return installationSummary; } } + /// /// Imports and saves package xml as /// @@ -123,7 +130,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Optional id of the User performing the operation. Default is zero (admin). /// An enumerable list of generated ContentTypes public IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId) - => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _mediaTypeService); + => ImportMediaTypes(docTypeElements, userId, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// Collection of entity containers installed by the package to be populated with those created in installing data types. + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportMediaTypes(IEnumerable docTypeElements, int userId, out IEnumerable entityContainersInstalled) + => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _mediaTypeService, out entityContainersInstalled); #endregion @@ -408,7 +425,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging #region DocumentTypes public IReadOnlyList ImportDocumentType(XElement docTypeElement, int userId) - => ImportDocumentTypes(new[] { docTypeElement }, userId); + => ImportDocumentTypes(new[] { docTypeElement }, userId, out _); /// /// Imports and saves package xml as @@ -417,7 +434,17 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Optional id of the User performing the operation. Default is zero (admin). /// An enumerable list of generated ContentTypes public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId) - => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService); + => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the User performing the operation. Default is zero (admin). + /// Collection of entity containers installed by the package to be populated with those created in installing data types. + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId, out IEnumerable entityContainersInstalled) + => ImportDocumentTypes(docTypeElements.ToList(), true, userId, _contentTypeService, out entityContainersInstalled); /// /// Imports and saves package xml as @@ -428,6 +455,18 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// An enumerable list of generated ContentTypes public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId, IContentTypeBaseService service) where T : class, IContentTypeComposition + => ImportDocumentTypes(unsortedDocumentTypes, importStructure, userId, service); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Boolean indicating whether or not to import the + /// Optional id of the User performing the operation. Default is zero (admin). + /// Collection of entity containers installed by the package to be populated with those created in installing data types. + /// An enumerable list of generated ContentTypes + public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId, IContentTypeBaseService service, out IEnumerable entityContainersInstalled) + where T : class, IContentTypeComposition { var importedContentTypes = new Dictionary(); @@ -436,7 +475,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging var graph = new TopoGraph>(x => x.Key, x => x.Dependencies); var isSingleDocTypeImport = unsortedDocumentTypes.Count == 1; - var importedFolders = CreateContentTypeFolderStructure(unsortedDocumentTypes); + var importedFolders = CreateContentTypeFolderStructure(unsortedDocumentTypes, out entityContainersInstalled); if (isSingleDocTypeImport == false) { @@ -532,9 +571,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging return list; } - private Dictionary CreateContentTypeFolderStructure(IEnumerable unsortedDocumentTypes) + private Dictionary CreateContentTypeFolderStructure(IEnumerable unsortedDocumentTypes, out IEnumerable entityContainersInstalled) { var importedFolders = new Dictionary(); + var trackEntityContainersInstalled = new List(); foreach (var documentType in unsortedDocumentTypes) { var foldersAttribute = documentType.Attribute("Folders"); @@ -578,8 +618,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); throw tryCreateFolder.Exception; } + var rootFolderId = tryCreateFolder.Result.Entity.Id; current = _contentTypeService.GetContainer(rootFolderId); + trackEntityContainersInstalled.Add(current); } importedFolders.Add(alias, current.Id); @@ -589,11 +631,13 @@ namespace Umbraco.Cms.Infrastructure.Packaging var folderName = WebUtility.UrlDecode(folders[i]); Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; current = CreateContentTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); + trackEntityContainersInstalled.Add(current); importedFolders[alias] = current.Id; } } } + entityContainersInstalled = trackEntityContainersInstalled; return importedFolders; } @@ -1012,10 +1056,20 @@ namespace Umbraco.Cms.Infrastructure.Packaging /// Optional id of the user /// An enumerable list of generated DataTypeDefinitions public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId) + => ImportDataTypes(dataTypeElements, userId, out _); + + /// + /// Imports and saves package xml as + /// + /// Xml to import + /// Optional id of the user + /// Collection of entity containers installed by the package to be populated with those created in installing data types. + /// An enumerable list of generated DataTypeDefinitions + public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId, out IEnumerable entityContainersInstalled) { var dataTypes = new List(); - var importedFolders = CreateDataTypeFolderStructure(dataTypeElements); + var importedFolders = CreateDataTypeFolderStructure(dataTypeElements, out entityContainersInstalled); foreach (var dataTypeElement in dataTypeElements) { @@ -1072,9 +1126,10 @@ namespace Umbraco.Cms.Infrastructure.Packaging return dataTypes; } - private Dictionary CreateDataTypeFolderStructure(IEnumerable datatypeElements) + private Dictionary CreateDataTypeFolderStructure(IEnumerable datatypeElements, out IEnumerable entityContainersInstalled) { var importedFolders = new Dictionary(); + var trackEntityContainersInstalled = new List(); foreach (var datatypeElement in datatypeElements) { var foldersAttribute = datatypeElement.Attribute("Folders"); @@ -1103,7 +1158,9 @@ namespace Umbraco.Cms.Infrastructure.Packaging _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); throw tryCreateFolder.Exception; } + current = _dataTypeService.GetContainer(tryCreateFolder.Result.Entity.Id); + trackEntityContainersInstalled.Add(current); } importedFolders.Add(name, current.Id); @@ -1113,11 +1170,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging var folderName = WebUtility.UrlDecode(folders[i]); Guid? folderKey = (folderKeys.Length == folders.Length) ? folderKeys[i] : null; current = CreateDataTypeChildFolder(folderName, folderKey ?? Guid.NewGuid(), current); + trackEntityContainersInstalled.Add(current); importedFolders[name] = current.Id; } } } - + entityContainersInstalled = trackEntityContainersInstalled; return importedFolders; } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs index 3166cdbd4f..54b96955d4 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageMigrationBase.cs @@ -1,12 +1,17 @@ +using System; +using System.ComponentModel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Infrastructure.Packaging { - public abstract class PackageMigrationBase : MigrationBase { private readonly IPackagingService _packagingService; @@ -15,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private readonly MediaUrlGeneratorCollection _mediaUrlGenerators; private readonly IShortStringHelper _shortStringHelper; private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly IOptions _packageMigrationsSettings; public PackageMigrationBase( IPackagingService packagingService, @@ -23,7 +29,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging MediaUrlGeneratorCollection mediaUrlGenerators, IShortStringHelper shortStringHelper, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, - IMigrationContext context) + IMigrationContext context, + IOptions packageMigrationsSettings) : base(context) { _packagingService = packagingService; @@ -32,6 +39,29 @@ namespace Umbraco.Cms.Infrastructure.Packaging _mediaUrlGenerators = mediaUrlGenerators; _shortStringHelper = shortStringHelper; _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _packageMigrationsSettings = packageMigrationsSettings; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use ctor with all params")] + public PackageMigrationBase( + IPackagingService packagingService, + IMediaService mediaService, + MediaFileManager mediaFileManager, + MediaUrlGeneratorCollection mediaUrlGenerators, + IShortStringHelper shortStringHelper, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IMigrationContext context) + : this( + packagingService, + mediaService, + mediaFileManager, + mediaUrlGenerators, + shortStringHelper, + contentTypeBaseServiceProvider, + context, + StaticServiceProvider.Instance.GetRequiredService>()) + { } public IImportPackageBuilder ImportPackage => BeginBuild( @@ -42,7 +72,8 @@ namespace Umbraco.Cms.Infrastructure.Packaging _mediaUrlGenerators, _shortStringHelper, _contentTypeBaseServiceProvider, - Context)); + Context, + _packageMigrationsSettings)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 57fc10ab7f..22e2480bad 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -553,6 +553,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { var isEntityDirty = entity.IsDirty(); + var editedSnapshot = entity.Edited; // check if we need to make any database changes at all if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) @@ -659,6 +660,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement edited = true; } + // To establish the new value of "edited" we compare all properties publishedValue to editedValue and look + // for differences. + // + // If we SaveAndPublish but the publish fails (e.g. already scheduled for release) + // we have lost the publishedValue on IContent (in memory vs database) so we cannot correctly make that comparison. + // + // This is a slight change to behaviour, historically a publish, followed by change & save, followed by undo change & save + // would change edited back to false. + if (!publishing && editedSnapshot) + { + edited = true; + } + if (entity.ContentType.VariesByCulture()) { // names also impact 'edited' diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs index 2e9fb6cebc..0b874a80c2 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs @@ -1,7 +1,5 @@ using System; -using System.Reflection; using NPoco; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence { @@ -18,6 +16,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence /// So far, this is very manual. We don't try to be clever and figure out whether the /// columns exist already. We just ignore it. /// Beware, the application MUST restart when this class behavior changes. + /// You can override the GetColmunnInfo method to control which columns this includes /// internal class UmbracoPocoDataBuilder : PocoDataBuilder { @@ -28,19 +27,5 @@ namespace Umbraco.Cms.Infrastructure.Persistence { _upgrading = upgrading; } - - protected override ColumnInfo GetColumnInfo(MemberInfo mi, Type type) - { - var columnInfo = base.GetColumnInfo(mi, type); - - // TODO: Is this upgrade flag still relevant? It's a lot of hacking to just set this value - // including the interface method ConfigureForUpgrade for this one circumstance. - if (_upgrading) - { - if (type == typeof(UserDto) && mi.Name == "TourData") columnInfo.IgnoreColumn = true; - } - - return columnInfo; - } } } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index ad35cbf30a..4ec87dfde7 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -1,9 +1,9 @@ using System; +using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; @@ -13,7 +13,9 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; +using ComponentCollection = Umbraco.Cms.Core.Composing.ComponentCollection; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -27,6 +29,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly IMainDom _mainDom; private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IEventAggregator _eventAggregator; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IUmbracoVersion _umbracoVersion; + private readonly IServiceProvider _serviceProvider; private CancellationToken _cancellationToken; /// @@ -40,7 +45,10 @@ namespace Umbraco.Cms.Infrastructure.Runtime IProfilingLogger profilingLogger, IMainDom mainDom, IUmbracoDatabaseFactory databaseFactory, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + IHostingEnvironment hostingEnvironment, + IUmbracoVersion umbracoVersion, + IServiceProvider serviceProvider) { State = state; _loggerFactory = loggerFactory; @@ -50,9 +58,42 @@ namespace Umbraco.Cms.Infrastructure.Runtime _mainDom = mainDom; _databaseFactory = databaseFactory; _eventAggregator = eventAggregator; + _hostingEnvironment = hostingEnvironment; + _umbracoVersion = umbracoVersion; + _serviceProvider = serviceProvider; _logger = _loggerFactory.CreateLogger(); } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete] + public CoreRuntime( + ILoggerFactory loggerFactory, + IRuntimeState state, + ComponentCollection components, + IApplicationShutdownRegistry applicationShutdownRegistry, + IProfilingLogger profilingLogger, + IMainDom mainDom, + IUmbracoDatabaseFactory databaseFactory, + IEventAggregator eventAggregator, + IHostingEnvironment hostingEnvironment, + IUmbracoVersion umbracoVersion + ):this( + loggerFactory, + state, + components, + applicationShutdownRegistry, + profilingLogger, + mainDom, + databaseFactory, + eventAggregator, + hostingEnvironment, + umbracoVersion, + null + ) + { + + } + /// /// Gets the state of the Umbraco runtime. /// @@ -70,6 +111,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime { _cancellationToken = cancellationToken; StaticApplicationLogging.Initialize(_loggerFactory); + StaticServiceProvider.Instance = _serviceProvider; AppDomain.CurrentDomain.UnhandledException += (_, args) => { diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index a3080570b7..3645115aa5 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Core.Security /// /// The user store for back office users /// - public class BackOfficeUserStore : UmbracoUserStore> + public class BackOfficeUserStore : UmbracoUserStore>, IUserSessionStore { private readonly IScopeProvider _scopeProvider; private readonly IUserService _userService; diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs index 15490bcd04..21c365dd8e 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs @@ -531,18 +531,20 @@ namespace Umbraco.Cms.Core.Services.Implement public IEnumerable GetAncestors(IContent content) { //null check otherwise we get exceptions - if (content.Path.IsNullOrWhiteSpace()) return Enumerable.Empty(); - - var rootId = Cms.Core.Constants.System.RootString; - var ids = content.Path.Split(Constants.CharArrays.Comma) - .Where(x => x != rootId && x != content.Id.ToString(CultureInfo.InvariantCulture)).Select(s => - int.Parse(s, CultureInfo.InvariantCulture)).ToArray(); - if (ids.Any() == false) - return new List(); - - using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + if (content.Path.IsNullOrWhiteSpace()) { - scope.ReadLock(Cms.Core.Constants.Locks.ContentTree); + return Enumerable.Empty(); + } + + var ids = content.GetAncestorIds().ToArray(); + if (ids.Any() == false) + { + return new List(); + } + + using (IScope scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); return _documentRepository.GetMany(ids); } } diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts new file mode 100644 index 0000000000..2cb5ce70c8 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts @@ -0,0 +1,47 @@ +/// + +function openSystemInformation(){ + //We have to wait for page to load, if the site is slow + cy.get('[data-element="global-help"]').should('be.visible').click(); + cy.get('.umb-help-list-item').last().should('be.visible').click(); + cy.get('.umb-drawer-content').scrollTo('bottom', {ensureScrollable : false}); +} + +context('System Information', () => { + + beforeEach(() => { + //arrange + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + cy.umbracoSetCurrentUserLanguage('en-US'); + }); + afterEach(() => { + cy.umbracoSetCurrentUserLanguage('en-US'); + }); + + it('Check System Info Displays', () => { + openSystemInformation(); + cy.get('.table').find('tr').should('have.length', 10); + cy.contains('Current Culture').parent().should('contain', 'en-US'); + cy.contains('Current UI Culture').parent().should('contain', 'en-US'); + }); + + it('Checks language displays correctly after switching', () => { + + //Navigate to edit user and change language + cy.umbracoGlobalUser().click(); + cy.get('[alias="editUser"]').click(); + cy.get('[name="culture"]').select('string:da-DK', { force: true}); + cy.umbracoButtonByLabelKey('buttons_save').click({force: true}); + //Refresh site to display new language + cy.reload(); + cy.get('.umb-tour-step', { timeout: 60000 }).should('be.visible'); // We now due to the api calls this will be shown, but slow computers can take a while + cy.get('.umb-tour-step__close').click(); + openSystemInformation(); + //Assert + cy.contains('Current Culture').parent().should('contain', 'da-DK'); + cy.contains('Current UI Culture').parent().should('contain', 'da-DK'); + cy.get('.umb-button__content').last().click(); + //Clean + cy.umbracoSetCurrentUserLanguage('en-US'); + }); +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts index 77729912a6..92365d7426 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts @@ -1,30 +1,306 @@ /// import { - DocumentTypeBuilder, - AliasHelper -} from 'umbraco-cypress-testhelpers'; - -const tabsDocTypeName = 'Tabs Test Document'; -const tabsDocTypeAlias = AliasHelper.toAlias(tabsDocTypeName); - -context('Tabs', () => { + DocumentTypeBuilder, + AliasHelper + } from 'umbraco-cypress-testhelpers'; - beforeEach(() => { - cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); - }); + const tabsDocTypeName = 'Tabs Test Document'; + const tabsDocTypeAlias = AliasHelper.toAlias(tabsDocTypeName); - afterEach(() => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName) - }); + context('Tabs', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); + }); + + afterEach(() => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName) + }); + + function OpenDocTypeFolder(){ + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + cy.get('.umb-tree-item__inner > .umb-tree-item__arrow').eq(0).click(); + cy.get('.umb-tree-item__inner > .umb-tree-item__label').contains(tabsDocTypeName).click(); + } + + function CreateDocWithTabAndNavigate(){ + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + } + + it('Create tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + cy.deleteAllContent(); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addGroup() + .withName('Tabs1Group') + .addUrlPickerProperty() + .withAlias('picker') + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Create a tab + cy.get('.umb-group-builder__tabs__add-tab').click(); + cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 1'); + //Create a 2nd tab manually + cy.get('.umb-group-builder__tabs__add-tab').click(); + cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 2'); + //Create a textstring property + cy.get('[aria-hidden="false"] > .umb-box-content > .umb-group-builder__group-add-property').click(); + cy.get('.editor-label').type('property name'); + cy.get('[data-element="editor-add"]').click(); - function OpenDocTypeFolder(){ - cy.umbracoSection('settings'); - cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); - cy.get('.umb-tree-item__inner > .umb-tree-item__arrow').eq(0).click(); - cy.get('.umb-tree-item__inner > .umb-tree-item__label').contains(tabsDocTypeName).click(); - } + //Search for textstring + cy.get('#datatype-search').type('Textstring'); - function CreateDocWithTabAndNavigate(){ + // Choose first item + cy.get('[title="Textstring"]').closest("li").click(); + + // Save property + cy.get('.btn-success').last().click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="tab1"]').should('be.visible'); + cy.get('[title="tab2"]').should('be.visible'); + }); + + it('Delete tabs', () => { + CreateDocWithTabAndNavigate(); + //Check if tab is there, else if it wasnt created, this test would always pass + cy.get('[title="aTab 1"]').should('be.visible'); + //Delete a tab + cy.get('.btn-reset > .icon-trash').click(); + cy.get('.umb-button > .btn').last().click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.get('[title="aTab 1"]').should('not.exist'); + //Clean + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + }); + + it('Delete property in tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .addContentPickerProperty() + .withAlias('picker') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[aria-label="Delete property"]').last().click(); + cy.umbracoButtonByLabelKey('actions_delete').click(); + cy.umbracoButtonByLabelKey('buttons_save').click() + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title=urlPicker]').should('be.visible'); + cy.get('[title=picker]').should('not.exist'); + }); + + it('Delete group in tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .addGroup() + .withName('Content Picker Group') + .addContentPickerProperty() + .withAlias('picker') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Delete group + cy.get('.umb-group-builder__group-remove > .icon-trash').eq(1).click(); + cy.umbracoButtonByLabelKey('actions_delete').click(); + cy.umbracoButtonByLabelKey('buttons_save').click() + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title=picker]').should('be.visible'); + cy.get('[title=urlPicker]').should('not.exist'); + }); + + it('Reorders tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group 1') + .addUrlPickerProperty() + .withLabel('Url picker 1') + .withAlias("urlPicker") + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withLabel('Url picker 2') + .withAlias("pickerTab 2") + .done() + .done() + .done() + .addTab() + .withName('Tab 3') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withLabel('Url picker 3') + .withAlias('pickerTab3') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Check if there are any tabs + cy.get('ng-form.ng-valid-required > .umb-group-builder__group-title-input').should('be.visible'); + cy.get('[alias="reorder"]').click(); + //Type order in + cy.get('.umb-group-builder__tab-sort-order > .umb-property-editor-tiny').first().type('3'); + cy.get('[alias="reorder"]').click(); + //Assert + cy.get('.umb-group-builder__group-title-input').eq(0).invoke('attr', 'title').should('eq', 'aTab 2') + cy.get('.umb-group-builder__group-title-input').eq(1).invoke('attr', 'title').should('eq', 'aTab 3') + cy.get('.umb-group-builder__group-title-input').eq(2).invoke('attr', 'title').should('eq', 'aTab 1') + }); + + it('Reorders groups in a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group 1') + .addUrlPickerProperty() + .withLabel('Url picker 1') + .withAlias("urlPicker") + .done() + .done() + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withLabel('Url picker 2') + .withAlias('urlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + cy.get('.umb-property-editor-tiny').eq(2).type('1'); + + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('.umb-group-builder__group-title-input').eq(2) + .invoke('attr', 'title').should('eq', 'aTab 1/aTab group 2'); + }); + + it('Reorders properties in a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withLabel('PickerOne') + .withAlias("urlPicker") + .done() + .addUrlPickerProperty() + .withLabel('PickerTwo') + .withAlias('urlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Reorder + cy.get('[alias="reorder"]').click(); + cy.get('.umb-group-builder__group-sort-value').first().type('2'); + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('.umb-locked-field__input').last().invoke('attr', 'title').should('eq', 'urlPicker'); + }); + + it('Tab name cannot be empty', () => { + CreateDocWithTabAndNavigate(); + cy.get('.umb-group-builder__group-title-input').first().clear(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); + + it('Two tabs cannot have the same name', () => { cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); const tabsDocType = new DocumentTypeBuilder() .withName(tabsDocTypeName) @@ -43,510 +319,234 @@ context('Tabs', () => { .build(); cy.saveDocumentType(tabsDocType); OpenDocTypeFolder(); - } + //Create a 2nd tab manually + cy.get('.umb-group-builder__tabs__add-tab').click(); + cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 1'); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); - it('Create tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - cy.deleteAllContent(); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addGroup() - .withName('Tabs1Group') - .addUrlPickerProperty() - .withAlias('picker') - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - //Create a tab - cy.get('.umb-group-builder__tabs__add-tab').click(); - cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 1'); - //Create a 2nd tab manually - cy.get('.umb-group-builder__tabs__add-tab').click(); - cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 2'); - //Create a textstring property - cy.get('[aria-hidden="false"] > .umb-box-content > .umb-group-builder__group-add-property').click(); - cy.get('.editor-label').type('property name'); - cy.get('[data-element="editor-add"]').click(); - - //Search for textstring - cy.get('#datatype-search').type('Textstring'); - - // Choose first item - cy.get('[title="Textstring"]').closest("li").click(); - - // Save property - cy.get('.btn-success').last().click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title="tab1"]').should('be.visible'); - cy.get('[title="tab2"]').should('be.visible'); - }); - - it('Delete tabs', () => { - CreateDocWithTabAndNavigate(); - //Check if tab is there, else if it wasnt created, this test would always pass - cy.get('[title="aTab 1"]').should('be.visible'); - //Delete a tab - cy.get('.btn-reset > .icon-trash').click(); - cy.get('.umb-button > .btn').last().click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.get('[title="aTab 1"]').should('not.exist'); - //Clean - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - }); + it('Group name cannot be empty', () => { + CreateDocWithTabAndNavigate(); + cy.get('.clearfix > .-placeholder').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); - it('Delete property in tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withAlias("urlPicker") - .done() - .addContentPickerProperty() - .withAlias('picker') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - cy.get('[aria-label="Delete property"]').last().click(); - cy.umbracoButtonByLabelKey('actions_delete').click(); - cy.umbracoButtonByLabelKey('buttons_save').click() - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title=urlPicker]').should('be.visible'); - cy.get('[title=picker]').should('not.exist'); - }); + it('Group name cannot have the same name', () => { + CreateDocWithTabAndNavigate(); + cy.get('.clearfix > .-placeholder').click(); + cy.get('.umb-group-builder__group-title-input').last().type('Tab group'); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); - it('Delete group in tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withAlias("urlPicker") + it('Drag a group into another tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() .done() - .done() - .addGroup() - .withName('Content Picker Group') - .addContentPickerProperty() - .withAlias('picker') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - //Delete group - cy.get('.umb-group-builder__group-remove > .icon-trash').eq(1).click(); - cy.umbracoButtonByLabelKey('actions_delete').click(); - cy.umbracoButtonByLabelKey('buttons_save').click() - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title=picker]').should('be.visible'); - cy.get('[title=urlPicker]').should('not.exist'); - }); - - it('Reorders tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group 1') - .addUrlPickerProperty() - .withLabel('Url picker 1') - .withAlias("urlPicker") - .done() - .done() - .done() - .addTab() + .done() + .addTab() .withName('Tab 2') .addGroup() - .withName('Tab group 2') + .withName('Tab group tab 2') .addUrlPickerProperty() - .withLabel('Url picker 2') - .withAlias("pickerTab 2") - .done() - .done() - .done() - .addTab() - .withName('Tab 3') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withLabel('Url picker 3') - .withAlias('pickerTab3') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - //Check if there are any tabs - cy.get('ng-form.ng-valid-required > .umb-group-builder__group-title-input').should('be.visible'); - cy.get('[alias="reorder"]').click(); - //Type order in - cy.get('.umb-group-builder__tab-sort-order > .umb-property-editor-tiny').first().type('3'); - cy.get('[alias="reorder"]').click(); - //Assert - cy.get('.umb-group-builder__group-title-input').eq(0).invoke('attr', 'title').should('eq', 'aTab 2') - cy.get('.umb-group-builder__group-title-input').eq(1).invoke('attr', 'title').should('eq', 'aTab 3') - cy.get('.umb-group-builder__group-title-input').eq(2).invoke('attr', 'title').should('eq', 'aTab 1') - }); - - it('Reorders groups in a tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group 1') - .addUrlPickerProperty() - .withLabel('Url picker 1') - .withAlias("urlPicker") + .withAlias('urlPickerTabTwo') .done() .done() .addGroup() - .withName('Tab group 2') - .addUrlPickerProperty() - .withLabel('Url picker 2') - .withAlias('urlPickerTwo') + .withName('Tab group 2') + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .done() .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - cy.get('[alias="reorder"]').click(); - cy.get('.umb-property-editor-tiny').eq(2).type('1'); - - cy.get('[alias="reorder"]').click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('.umb-group-builder__group-title-input').eq(2) - .invoke('attr', 'title').should('eq', 'aTab 1/aTab group 2'); - }); - - it('Reorders properties in a tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withLabel('PickerOne') - .withAlias("urlPicker") - .done() - .addUrlPickerProperty() - .withLabel('PickerTwo') - .withAlias('urlPickerTwo') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - //Reorder - cy.get('[alias="reorder"]').click(); - cy.get('.umb-group-builder__group-sort-value').first().type('2'); - cy.get('[alias="reorder"]').click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('.umb-locked-field__input').last().invoke('attr', 'title').should('eq', 'urlPicker'); - }); - - it('Tab name cannot be empty', () => { - CreateDocWithTabAndNavigate(); - cy.get('.umb-group-builder__group-title-input').first().clear(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoErrorNotification().should('be.visible'); - }); - - it('Two tabs cannot have the same name', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withAlias("urlPicker") - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - //Create a 2nd tab manually - cy.get('.umb-group-builder__tabs__add-tab').click(); - cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 1'); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoErrorNotification().should('be.visible'); - }); - - it('Group name cannot be empty', () => { - CreateDocWithTabAndNavigate(); - cy.get('.clearfix > .-placeholder').click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoErrorNotification().should('be.visible'); - }); - - it('Group name cannot have the same name', () => { - CreateDocWithTabAndNavigate(); - cy.get('.clearfix > .-placeholder').click(); - cy.get('.umb-group-builder__group-title-input').last().type('Tab group'); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoErrorNotification().should('be.visible'); - }); - - it('Drag a group into another tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withAlias("urlPicker") - .done() - .done() - .done() - .addTab() - .withName('Tab 2') - .addGroup() - .withName('Tab group tab 2') - .addUrlPickerProperty() - .withAlias('urlPickerTabTwo') - .done() .done() - .addGroup() - .withName('Tab group 2') - .addUrlPickerProperty() - .withAlias('urlPickerTwo') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - cy.get('[alias="reorder"]').click(); - cy.get('body') - .then(($body) => { - while($body.find('.umb-group-builder__tabs-overflow--right > .caret').hasClass('active')){ - cy.click(); - } - }); - cy.get('.umb-group-builder__tab').last().click(); - cy.get('.umb-group-builder__group-title-icon').last().trigger('mousedown', { which: 1 }) - cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); - cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active').trigger('mouseup', {force:true}); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title="aTab 1/aTab group 2"]').should('be.visible'); - }); - - it('Drag and drop reorders a tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withAlias("urlPicker") - .done() - .done() - .done() - .addTab() - .withName('Tab 2') - .addGroup() - .withName('Tab group tab 2') - .addUrlPickerProperty() - .withAlias('urlPickerTabTwo') - .done() - .done() - .addGroup() - .withName('Tab group 2') - .addUrlPickerProperty() - .withAlias('urlPickerTwo') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - cy.get('[alias="reorder"]').click(); - //Scroll right so we can see tab 2 - cy.get('body') + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + cy.get('body') .then(($body) => { while($body.find('.umb-group-builder__tabs-overflow--right > .caret').hasClass('active')){ cy.click(); } }); - cy.get('.umb-group-builder__tab-title-icon').eq(1).trigger('mousedown', { which: 1 }) - cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); - cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active').trigger('mouseup', {force:true}); - cy.get('[alias="reorder"]').click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title="aTab 2"]').should('be.visible'); - }); - - it('Drags and drops a property in a tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') - .addGroup() - .withName('Tab group') - .addUrlPickerProperty() - .withAlias("urlPicker") - .withLabel('UrlPickerOne') - .done() - .done() - .done() - .addTab() - .withName('Tab 2') - .addGroup() - .withName('Tab group tab 2') - .addUrlPickerProperty() - .withAlias('urlPickerTabTwo') - .withLabel('UrlPickerTabTwo') - .done() - .addUrlPickerProperty() - .withAlias('urlPickerTwo') - .withLabel('UrlPickerTwo') - .done() - .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - cy.get('[alias="reorder"]').click(); - //Scroll so we are sure we see tab 2 - cy.get('body') - .then(($body) => { - while($body.find('.umb-group-builder__tabs-overflow--right > .caret').hasClass('active')){ - cy.click(); - } + cy.get('.umb-group-builder__tab').last().click(); + cy.get('.umb-group-builder__group-title-icon').last().trigger('mousedown', { which: 1 }) + cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active').trigger('mouseup', {force:true}); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="aTab 1/aTab group 2"]').should('be.visible'); }); - //Navigate to tab 2 - cy.get('.umb-group-builder__tab').last().click(); - cy.get('.umb-group-builder__property-meta > .flex > .icon').eq(1).trigger('mousedown', {which: 1}) - cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); - cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active'); - cy.get('.umb-group-builder__property') - .first().trigger('mousemove', {which: 1, force: true}).trigger('mouseup', {which: 1, force:true}); - cy.get('[alias="reorder"]').click(); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title="urlPickerTabTwo"]').should('be.visible'); - }); - - it('Drags and drops a group and converts to tab', () => { - cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); - const tabsDocType = new DocumentTypeBuilder() - .withName(tabsDocTypeName) - .withAlias(tabsDocTypeAlias) - .withAllowAsRoot(true) - .withDefaultTemplate(tabsDocTypeAlias) - .addTab() - .withName('Tab 1') + + it('Drag and drop reorders a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .done() + .addTab() + .withName('Tab 2') .addGroup() - .withName('Tab group') + .withName('Tab group tab 2') .addUrlPickerProperty() - .withAlias("urlPicker") - .withLabel('UrlPickerOne') + .withAlias('urlPickerTabTwo') .done() .done() .addGroup() - .withName('Tab group 2') - .addUrlPickerProperty() - .withAlias('urlPickerTwo') - .withLabel('UrlPickerTwo') + .withName('Tab group 2') + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .done() .done() .done() - .done() - .addTab() - .withName('Tab 2') - .addGroup() - .withName('Tab group tab 2') - .addUrlPickerProperty() - .withAlias('urlPickerTabTwo') - .withLabel('UrlPickerTabTwo') + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + //Scroll right so we can see tab 2 + cy.get('body') + .then(($body) => { + while($body.find('.umb-group-builder__tabs-overflow--right > .caret').hasClass('active')){ + cy.click(); + } + }); + cy.get('.umb-group-builder__tab-title-icon').eq(1).trigger('mousedown', { which: 1 }) + cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active').trigger('mouseup', {force:true}); + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="aTab 2"]').should('be.visible'); + }); + + it('Drags and drops a property in a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .withLabel('UrlPickerOne') + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group tab 2') + .addUrlPickerProperty() + .withAlias('urlPickerTabTwo') + .withLabel('UrlPickerTabTwo') + .done() + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .withLabel('UrlPickerTwo') + .done() .done() .done() - .done() - .build(); - cy.saveDocumentType(tabsDocType); - OpenDocTypeFolder(); - cy.get('[alias="reorder"]').click(); - cy.get('.umb-group-builder__group-title-icon').eq(1).trigger('mousedown', {which: 1}) - cy.get('.umb-group-builder__convert-dropzone').trigger('mousemove', {which: 1, force: true}); - cy.get('.umb-group-builder__convert-dropzone').trigger('mouseup', {which: 1, force:true}); - cy.umbracoButtonByLabelKey('buttons_save').click(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); - cy.get('[title="tabGroup"]').should('be.visible'); - }); -}); \ No newline at end of file + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + //Scroll so we are sure we see tab 2 + cy.get('body') + .then(($body) => { + while($body.find('.umb-group-builder__tabs-overflow--right > .caret').hasClass('active')){ + cy.click(); + } + }); + //Navigate to tab 2 + cy.get('.umb-group-builder__tab').last().click(); + cy.get('.umb-group-builder__property-meta > .flex > .icon').eq(1).trigger('mousedown', {which: 1}) + cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active'); + cy.get('.umb-group-builder__property') + .first().trigger('mousemove', {which: 1, force: true}).trigger('mouseup', {which: 1, force:true}); + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="urlPickerTabTwo"]').should('be.visible'); + }); + + it('Drags and drops a group and converts to tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .withLabel('UrlPickerOne') + .done() + .done() + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .withLabel('UrlPickerTwo') + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group tab 2') + .addUrlPickerProperty() + .withAlias('urlPickerTabTwo') + .withLabel('UrlPickerTabTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + cy.get('.umb-group-builder__group-title-icon').eq(1).trigger('mousedown', {which: 1}) + cy.get('.umb-group-builder__convert-dropzone').trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__convert-dropzone').trigger('mouseup', {which: 1, force:true}); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="tabGroup"]').should('be.visible'); + }); + }); \ No newline at end of file diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts similarity index 99% rename from src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts rename to src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts index d3950d7d19..8bdd052e94 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts @@ -100,4 +100,4 @@ function runBackOfficeIntroTour(percentageComplete, buttonText, timeout) { cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); cy.umbracoGlobalHelp().should("be.visible"); -} +} \ No newline at end of file diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index 2b85faaf9c..20a5a774e0 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -11,7 +11,7 @@ "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-57" + "umbraco-cypress-testhelpers": "^1.0.0-beta-58" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests.Common/Builders/ContentBuilder.cs b/src/Umbraco.Tests.Common/Builders/ContentBuilder.cs index 6602c7b25c..4521411352 100644 --- a/src/Umbraco.Tests.Common/Builders/ContentBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/ContentBuilder.cs @@ -16,6 +16,7 @@ namespace Umbraco.Cms.Tests.Common.Builders public class ContentBuilder : BuilderBase, IBuildContentTypes, + IBuildContentCultureInfosCollection, IWithIdBuilder, IWithKeyBuilder, IWithParentIdBuilder, @@ -31,6 +32,7 @@ namespace Umbraco.Cms.Tests.Common.Builders IWithPropertyValues { private ContentTypeBuilder _contentTypeBuilder; + private ContentCultureInfosCollectionBuilder _contentCultureInfosCollectionBuilder; private GenericDictionaryBuilder _propertyDataBuilder; private int? _id; @@ -48,6 +50,7 @@ namespace Umbraco.Cms.Tests.Common.Builders private bool? _trashed; private CultureInfo _cultureInfo; private IContentType _contentType; + private ContentCultureInfosCollection _contentCultureInfosCollection; private readonly IDictionary _cultureNames = new Dictionary(); private object _propertyValues; private string _propertyValuesCulture; @@ -73,6 +76,14 @@ namespace Umbraco.Cms.Tests.Common.Builders return this; } + public ContentBuilder WithContentCultureInfosCollection( + ContentCultureInfosCollection contentCultureInfosCollection) + { + _contentCultureInfosCollectionBuilder = null; + _contentCultureInfosCollection = contentCultureInfosCollection; + return this; + } + public ContentBuilder WithCultureName(string culture, string name = "") { if (string.IsNullOrWhiteSpace(name)) @@ -105,6 +116,14 @@ namespace Umbraco.Cms.Tests.Common.Builders return builder; } + public ContentCultureInfosCollectionBuilder AddContentCultureInfosCollection() + { + _contentCultureInfosCollection = null; + var builder = new ContentCultureInfosCollectionBuilder(this); + _contentCultureInfosCollectionBuilder = builder; + return builder; + } + public override Content Build() { var id = _id ?? 0; @@ -176,6 +195,13 @@ namespace Umbraco.Cms.Tests.Common.Builders content.ResetDirtyProperties(false); } + if (_contentCultureInfosCollection is not null || _contentCultureInfosCollectionBuilder is not null) + { + ContentCultureInfosCollection contentCultureInfos = + _contentCultureInfosCollection ?? _contentCultureInfosCollectionBuilder.Build(); + content.PublishCultureInfos = contentCultureInfos; + } + return content; } diff --git a/src/Umbraco.Tests.Common/Builders/ContentCultureInfosBuilder.cs b/src/Umbraco.Tests.Common/Builders/ContentCultureInfosBuilder.cs new file mode 100644 index 0000000000..ba99bcbcbd --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/ContentCultureInfosBuilder.cs @@ -0,0 +1,45 @@ +using System; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders +{ + public class ContentCultureInfosBuilder : ChildBuilderBase, + IWithNameBuilder, + IWithDateBuilder + { + private string _name; + private string _cultureIso; + private DateTime? _date; + public ContentCultureInfosBuilder(ContentCultureInfosCollectionBuilder parentBuilder) : base(parentBuilder) + { + } + + public ContentCultureInfosBuilder WithCultureIso(string cultureIso) + { + _cultureIso = cultureIso; + return this; + } + + public override ContentCultureInfos Build() + { + var name = _name ?? Guid.NewGuid().ToString(); + var cultureIso = _cultureIso ?? "en-us"; + DateTime date = _date ?? DateTime.Now; + + return new ContentCultureInfos(cultureIso) { Name = name, Date = date }; + } + + public string Name + { + get => _name; + set => _name = value; + } + + public DateTime? Date + { + get => _date; + set => _date = value; + } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/ContentCultureInfosCollectionBuilder.cs b/src/Umbraco.Tests.Common/Builders/ContentCultureInfosCollectionBuilder.cs new file mode 100644 index 0000000000..a090880633 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/ContentCultureInfosCollectionBuilder.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders +{ + public class ContentCultureInfosCollectionBuilder : ChildBuilderBase, IBuildContentCultureInfosCollection + { + private readonly List _cultureInfosBuilders; + public ContentCultureInfosCollectionBuilder(ContentBuilder parentBuilder) : base(parentBuilder) => _cultureInfosBuilders = new List(); + + public ContentCultureInfosBuilder AddCultureInfos() + { + var builder = new ContentCultureInfosBuilder(this); + _cultureInfosBuilders.Add(builder); + return builder; + } + + public override ContentCultureInfosCollection Build() + { + if (_cultureInfosBuilders.Count < 1) + { + throw new InvalidOperationException("You must add at least one culture infos to the collection builder"); + } + var cultureInfosCollection = new ContentCultureInfosCollection(); + + foreach (ContentCultureInfosBuilder cultureInfosBuilder in _cultureInfosBuilders) + { + cultureInfosCollection.Add(cultureInfosBuilder.Build()); + } + + return cultureInfosCollection; + } + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs index 872a6ac367..0ca260b2c9 100644 --- a/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs +++ b/src/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs @@ -234,5 +234,11 @@ namespace Umbraco.Cms.Tests.Common.Builders.Extensions builder.PropertyValuesSegment = segment; return builder; } + + public static T WithDate(this T builder, DateTime date) where T : IWithDateBuilder + { + builder.Date = date; + return builder; + } } } diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IBuildContentCultureInfosCollection.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IBuildContentCultureInfosCollection.cs new file mode 100644 index 0000000000..d47b844192 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IBuildContentCultureInfosCollection.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces +{ + public interface IBuildContentCultureInfosCollection + { + + } +} diff --git a/src/Umbraco.Tests.Common/Builders/Interfaces/IWithDateBuilder.cs b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithDateBuilder.cs new file mode 100644 index 0000000000..3d1c9bddb7 --- /dev/null +++ b/src/Umbraco.Tests.Common/Builders/Interfaces/IWithDateBuilder.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces +{ + public interface IWithDateBuilder + { + DateTime? Date { get; set; } + } +} diff --git a/src/Umbraco.Tests.Integration/AssemblyAttributes.cs b/src/Umbraco.Tests.Integration/AssemblyAttributes.cs new file mode 100644 index 0000000000..afa3bb903e --- /dev/null +++ b/src/Umbraco.Tests.Integration/AssemblyAttributes.cs @@ -0,0 +1,4 @@ +using NUnit.Framework; + +[assembly: SetCulture("en-US")] +[assembly: SetUICulture("en-US")] diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index d75d667b1c..ad189cc02a 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -1348,6 +1348,107 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, published.Result); } + [Test] + public void Failed_Publish_Should_Not_Update_Edited_State_When_Edited_True() + { + // Arrange + IContentService contentService = GetRequiredService(); + IContentTypeService contentTypeService = GetRequiredService(); + + IContentType contentType = new ContentTypeBuilder() + .WithId(0) + .AddPropertyType() + .WithAlias("header") + .WithValueStorageType(ValueStorageType.Integer) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithName("header") + .Done() + .WithContentVariation(ContentVariation.Nothing) + .Build(); + + contentTypeService.Save(contentType); + + Content content = new ContentBuilder() + .WithId(0) + .WithName("Home") + .WithContentType(contentType) + .AddPropertyData() + .WithKeyValue("header", "Cool header") + .Done() + .Build(); + + contentService.SaveAndPublish(content); + + content.Properties[0].SetValue("Foo", culture: string.Empty); + content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); + contentService.Save(content); + + // Act + var result = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.IsTrue(result.Content.Published); + Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, result.Result); + + // We changed property data + Assert.IsTrue(result.Content.Edited, "result.Content.Edited"); + }); + } + + // V9 - Tests.Integration + [Test] + public void Failed_Publish_Should_Not_Update_Edited_State_When_Edited_False() + { + // Arrange + IContentService contentService = GetRequiredService(); + IContentTypeService contentTypeService = GetRequiredService(); + + IContentType contentType = new ContentTypeBuilder() + .WithId(0) + .AddPropertyType() + .WithAlias("header") + .WithValueStorageType(ValueStorageType.Integer) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithName("header") + .Done() + .WithContentVariation(ContentVariation.Nothing) + .Build(); + + contentTypeService.Save(contentType); + + Content content = new ContentBuilder() + .WithId(0) + .WithName("Home") + .WithContentType(contentType) + .AddPropertyData() + .WithKeyValue("header", "Cool header") + .Done() + .Build(); + + contentService.SaveAndPublish(content); + + content.ContentSchedule.Add(DateTime.Now.AddHours(2), null); + contentService.Save(content); + + // Act + var result = contentService.SaveAndPublish(content, userId: Constants.Security.SuperUserId); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.IsTrue(result.Content.Published); + Assert.AreEqual(PublishResultType.FailedPublishAwaitingRelease, result.Result); + + // We didn't change any property data + Assert.IsFalse(result.Content.Edited, "result.Content.Edited"); + }); + } + + [Test] public void Cannot_Publish_Culture_Awaiting_Release() { @@ -2151,7 +2252,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services ContentService.Save(rollback2); Assert.IsTrue(rollback2.Published); - Assert.IsFalse(rollback2.Edited); // all changes cleared! + Assert.IsTrue(rollback2.Edited); // Still edited, change of behaviour Assert.AreEqual("Jane Doe", rollback2.GetValue("author")); Assert.AreEqual("Text Page 2 ReReUpdated", rollback2.Name); @@ -2170,7 +2271,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services content.CopyFrom(rollto); content.Name = rollto.PublishName; // must do it explicitely AND must pick the publish one! ContentService.Save(content); - Assert.IsFalse(content.Edited); + Assert.IsTrue(content.Edited); //Still edited, change of behaviour Assert.AreEqual("Text Page 2 ReReUpdated", content.Name); Assert.AreEqual("Jane Doe", content.GetValue("author")); } diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs index ab2acf9825..b91e87907a 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs @@ -23,6 +23,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers [TestFixture] public class ContentControllerTests : UmbracoTestServerTestBase { + private const string UsIso = "en-US"; + private const string DkIso = "da-DK"; + /// /// Returns 404 if the content wasn't found based on the ID specified /// @@ -33,7 +36,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers // Add another language localizationService.Save(new LanguageBuilder() - .WithCultureInfo("da-DK") + .WithCultureInfo(DkIso) .WithIsDefault(false) .Build()); @@ -91,7 +94,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers // Add another language localizationService.Save(new LanguageBuilder() - .WithCultureInfo("da-DK") + .WithCultureInfo(DkIso) .WithIsDefault(false) .Build()); @@ -160,7 +163,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers // Add another language localizationService.Save(new LanguageBuilder() - .WithCultureInfo("da-DK") + .WithCultureInfo(DkIso) .WithIsDefault(false) .Build()); @@ -225,7 +228,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers // Add another language localizationService.Save(new LanguageBuilder() - .WithCultureInfo("da-DK") + .WithCultureInfo(DkIso) .WithIsDefault(false) .Build()); @@ -286,7 +289,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers // Add another language localizationService.Save(new LanguageBuilder() - .WithCultureInfo("da-DK") + .WithCultureInfo(DkIso) .WithIsDefault(false) .Build()); @@ -350,7 +353,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers // Add another language localizationService.Save(new LanguageBuilder() - .WithCultureInfo("da-DK") + .WithCultureInfo(DkIso) .WithIsDefault(false) .Build()); @@ -374,8 +377,8 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers Content content = new ContentBuilder() .WithId(0) - .WithCultureName("en-US", "English") - .WithCultureName("da-DK", "Danish") + .WithCultureName(UsIso, "English") + .WithCultureName(DkIso, "Danish") .WithContentType(contentType) .AddPropertyData() .WithKeyValue("title", "Cool invariant title") @@ -406,5 +409,291 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Controllers CollectionAssert.Contains(display.Errors.Keys, "_content_variant_en-US_null_"); }); } + + [Test] + public async Task PostSave_Validates_Domains_Exist() + { + ILocalizationService localizationService = GetRequiredService(); + localizationService.Save(new LanguageBuilder() + .WithCultureInfo(DkIso) + .WithIsDefault(false) + .Build()); + + IContentTypeService contentTypeService = GetRequiredService(); + IContentType contentType = new ContentTypeBuilder().WithContentVariation(ContentVariation.Culture).Build(); + contentTypeService.Save(contentType); + + Content content = new ContentBuilder() + .WithId(1) + .WithContentType(contentType) + .WithCultureName(UsIso, "Root") + .WithCultureName(DkIso, "Rod") + .Build(); + + ContentItemSave model = new ContentItemSaveBuilder() + .WithContent(content) + .WithAction(ContentSaveAction.PublishNew) + .Build(); + + var url = PrepareApiControllerUrl(x => x.PostSave(null)); + + HttpResponseMessage response = await Client.PostAsync(url, new MultipartFormDataContent + { + { new StringContent(JsonConvert.SerializeObject(model)), "contentItem" } + }); + + var body = await response.Content.ReadAsStringAsync(); + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + ContentItemDisplay display = JsonConvert.DeserializeObject(body); + + ILocalizedTextService localizedTextService = GetRequiredService(); + var expectedMessage = localizedTextService.Localize("speechBubbles", "publishWithNoDomains"); + + Assert.Multiple(() => + { + Assert.IsNotNull(display); + Assert.AreEqual(1, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + Assert.AreEqual(expectedMessage, display.Notifications.FirstOrDefault(x => x.NotificationType == NotificationStyle.Warning)?.Message); + }); + } + + [Test] + public async Task PostSave_Validates_All_Ancestor_Cultures_Are_Considered() + { + var sweIso = "sv-SE"; + ILocalizationService localizationService = GetRequiredService(); + //Create 2 new languages + localizationService.Save(new LanguageBuilder() + .WithCultureInfo(DkIso) + .WithIsDefault(false) + .Build()); + + localizationService.Save(new LanguageBuilder() + .WithCultureInfo(sweIso) + .WithIsDefault(false) + .Build()); + + IContentTypeService contentTypeService = GetRequiredService(); + IContentType contentType = new ContentTypeBuilder().WithContentVariation(ContentVariation.Culture).Build(); + contentTypeService.Save(contentType); + + Content content = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithCultureName(UsIso, "Root") + .Build(); + + IContentService contentService = GetRequiredService(); + contentService.SaveAndPublish(content); + + Content childContent = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithParent(content) + .WithCultureName(DkIso, "Barn") + .WithCultureName(UsIso, "Child") + .Build(); + + contentService.SaveAndPublish(childContent); + + Content grandChildContent = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithParent(childContent) + .WithCultureName(sweIso, "Bjarn") + .Build(); + + + ContentItemSave model = new ContentItemSaveBuilder() + .WithContent(grandChildContent) + .WithParentId(childContent.Id) + .WithAction(ContentSaveAction.PublishNew) + .Build(); + + ILanguage enLanguage = localizationService.GetLanguageByIsoCode(UsIso); + IDomainService domainService = GetRequiredService(); + var enDomain = new UmbracoDomain("/en") + { + RootContentId = content.Id, + LanguageId = enLanguage.Id + }; + domainService.Save(enDomain); + + ILanguage dkLanguage = localizationService.GetLanguageByIsoCode(DkIso); + var dkDomain = new UmbracoDomain("/dk") + { + RootContentId = childContent.Id, + LanguageId = dkLanguage.Id + }; + domainService.Save(dkDomain); + + var url = PrepareApiControllerUrl(x => x.PostSave(null)); + + HttpResponseMessage response = await Client.PostAsync(url, new MultipartFormDataContent + { + { new StringContent(JsonConvert.SerializeObject(model)), "contentItem" } + }); + + var body = await response.Content.ReadAsStringAsync(); + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + ContentItemDisplay display = JsonConvert.DeserializeObject(body); + + + ILocalizedTextService localizedTextService = GetRequiredService(); + var expectedMessage = localizedTextService.Localize("speechBubbles", "publishWithMissingDomain", new []{"sv-SE"}); + + Assert.Multiple(() => + { + Assert.NotNull(display); + Assert.AreEqual(1, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + Assert.AreEqual(expectedMessage, display.Notifications.FirstOrDefault(x => x.NotificationType == NotificationStyle.Warning)?.Message); + }); + } + + [Test] + public async Task PostSave_Validates_All_Cultures_Has_Domains() + { + ILocalizationService localizationService = GetRequiredService(); + localizationService.Save(new LanguageBuilder() + .WithCultureInfo(DkIso) + .WithIsDefault(false) + .Build()); + + IContentTypeService contentTypeService = GetRequiredService(); + IContentType contentType = new ContentTypeBuilder().WithContentVariation(ContentVariation.Culture).Build(); + contentTypeService.Save(contentType); + + Content content = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithCultureName(UsIso, "Root") + .WithCultureName(DkIso, "Rod") + .Build(); + + IContentService contentService = GetRequiredService(); + contentService.Save(content); + + ContentItemSave model = new ContentItemSaveBuilder() + .WithContent(content) + .WithAction(ContentSaveAction.Publish) + .Build(); + + ILanguage dkLanguage = localizationService.GetLanguageByIsoCode(DkIso); + IDomainService domainService = GetRequiredService(); + var dkDomain = new UmbracoDomain("/") + { + RootContentId = content.Id, + LanguageId = dkLanguage.Id + }; + domainService.Save(dkDomain); + + var url = PrepareApiControllerUrl(x => x.PostSave(null)); + + HttpResponseMessage response = await Client.PostAsync(url, new MultipartFormDataContent + { + { new StringContent(JsonConvert.SerializeObject(model)), "contentItem" } + }); + + var body = await response.Content.ReadAsStringAsync(); + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + ContentItemDisplay display = JsonConvert.DeserializeObject(body); + + + ILocalizedTextService localizedTextService = GetRequiredService(); + var expectedMessage = localizedTextService.Localize("speechBubbles", "publishWithMissingDomain", new []{UsIso}); + + Assert.Multiple(() => + { + Assert.NotNull(display); + Assert.AreEqual(1, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + Assert.AreEqual(expectedMessage, display.Notifications.FirstOrDefault(x => x.NotificationType == NotificationStyle.Warning)?.Message); + }); + } + + [Test] + public async Task PostSave_Checks_Ancestors_For_Domains() + { + ILocalizationService localizationService = GetRequiredService(); + localizationService.Save(new LanguageBuilder() + .WithCultureInfo(DkIso) + .WithIsDefault(false) + .Build()); + + IContentTypeService contentTypeService = GetRequiredService(); + IContentType contentType = new ContentTypeBuilder().WithContentVariation(ContentVariation.Culture).Build(); + contentTypeService.Save(contentType); + + Content rootNode = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithCultureName(UsIso, "Root") + .WithCultureName(DkIso, "Rod") + .Build(); + + IContentService contentService = GetRequiredService(); + contentService.SaveAndPublish(rootNode); + + Content childNode = new ContentBuilder() + .WithoutIdentity() + .WithParent(rootNode) + .WithContentType(contentType) + .WithCultureName(DkIso, "Barn") + .WithCultureName(UsIso, "Child") + .Build(); + + contentService.SaveAndPublish(childNode); + + Content grandChild = new ContentBuilder() + .WithoutIdentity() + .WithParent(childNode) + .WithContentType(contentType) + .WithCultureName(DkIso, "BarneBarn") + .WithCultureName(UsIso, "GrandChild") + .Build(); + + contentService.Save(grandChild); + + ILanguage dkLanguage = localizationService.GetLanguageByIsoCode(DkIso); + ILanguage usLanguage = localizationService.GetLanguageByIsoCode(UsIso); + IDomainService domainService = GetRequiredService(); + var dkDomain = new UmbracoDomain("/") + { + RootContentId = rootNode.Id, + LanguageId = dkLanguage.Id + }; + + var usDomain = new UmbracoDomain("/en") + { + RootContentId = childNode.Id, + LanguageId = usLanguage.Id + }; + + domainService.Save(dkDomain); + domainService.Save(usDomain); + + var url = PrepareApiControllerUrl(x => x.PostSave(null)); + + ContentItemSave model = new ContentItemSaveBuilder() + .WithContent(grandChild) + .WithAction(ContentSaveAction.Publish) + .Build(); + + HttpResponseMessage response = await Client.PostAsync(url, new MultipartFormDataContent + { + { new StringContent(JsonConvert.SerializeObject(model)), "contentItem" } + }); + + var body = await response.Content.ReadAsStringAsync(); + body = body.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + ContentItemDisplay display = JsonConvert.DeserializeObject(body); + + Assert.Multiple(() => + { + Assert.NotNull(display); + // Assert all is good, a success notification for each culture published and no warnings. + Assert.AreEqual(2, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Success)); + Assert.AreEqual(0, display.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + }); + } } } diff --git a/src/Umbraco.Tests.UnitTests/AssemblyAttributes.cs b/src/Umbraco.Tests.UnitTests/AssemblyAttributes.cs new file mode 100644 index 0000000000..afa3bb903e --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/AssemblyAttributes.cs @@ -0,0 +1,4 @@ +using NUnit.Framework; + +[assembly: SetCulture("en-US")] +[assembly: SetUICulture("en-US")] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs index 1a2ffa8011..c4cd4f0c02 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -29,6 +30,33 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Deploy Assert.AreEqual(expected, serialized); } + [Test] + public void Dependencies_Are_Correctly_Ordered() + { + // This test was introduced following: https://github.com/umbraco/Umbraco.Deploy.Issues/issues/72 to verify + // that consistent ordering rules are used across platforms. + var udi = new GuidUdi("test", Guid.Parse("3382d5433b5749d08919bc9961422a1f")); + var artifact = new TestArtifact(udi, new List()) + { + Name = "Test Name", + Alias = "testAlias", + }; + + var dependencies = new ArtifactDependencyCollection(); + + var dependencyUdi1 = new GuidUdi("template", Guid.Parse("d4651496fad24c1290a53ea4d55d945b")); + dependencies.Add(new ArtifactDependency(dependencyUdi1, true, ArtifactDependencyMode.Exist)); + + var dependencyUdi2 = new StringUdi(Constants.UdiEntityType.TemplateFile, "TestPage.cshtml"); + dependencies.Add(new ArtifactDependency(dependencyUdi2, true, ArtifactDependencyMode.Exist)); + + artifact.Dependencies = dependencies; + + Assert.AreEqual( + "umb://template-file/TestPage.cshtml,umb://template/d4651496fad24c1290a53ea4d55d945b", + string.Join(",", artifact.Dependencies.Select(x => x.Udi.ToString()))); + } + private class TestArtifact : ArtifactBase { public TestArtifact(GuidUdi udi, IEnumerable dependencies = null) : base(udi, dependencies) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs new file mode 100644 index 0000000000..7417976369 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services +{ + [TestFixture] + public class UserDataServiceTests + { + private IUmbracoVersion _umbracoVersion; + + [OneTimeSetUp] + public void CreateMocks() => CreateUmbracoVersion(9, 0, 0); + + [Test] + [TestCase("en-US")] + [TestCase("de-DE")] + [TestCase("en-NZ")] + [TestCase("sv-SE")] + public void GetCorrectDefaultLanguageTest(string culture) + { + var userDataService = CreateUserDataService(culture); + var defaultLanguage = userDataService.GetUserData().FirstOrDefault(x => x.Name == "Default Language"); + Assert.Multiple(() => + { + Assert.IsNotNull(defaultLanguage); + Assert.AreEqual(culture, defaultLanguage.Data); + }); + } + + [Test] + [TestCase("en-US")] + [TestCase("de-DE")] + [TestCase("en-NZ")] + [TestCase("sv-SE")] + public void GetCorrectCultureTest(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + var userDataService = CreateUserDataService(culture); + var currentCulture = userDataService.GetUserData().FirstOrDefault(x => x.Name == "Current Culture"); + Assert.Multiple(() => + { + Assert.IsNotNull(currentCulture); + Assert.AreEqual(culture, currentCulture.Data); + }); + } + + [Test] + [TestCase("en-US")] + [TestCase("de-DE")] + [TestCase("en-NZ")] + [TestCase("sv-SE")] + public void GetCorrectUICultureTest(string culture) + { + Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture); + var userDataService = CreateUserDataService(culture); + var currentCulture = userDataService.GetUserData().FirstOrDefault(x => x.Name == "Current UI Culture"); + Assert.Multiple(() => + { + Assert.IsNotNull(currentCulture); + Assert.AreEqual(culture, currentCulture.Data); + }); + } + + [Test] + [TestCase("en-US")] + [TestCase("de-DE")] + [TestCase("en-NZ")] + [TestCase("sv-SE")] + public void RunTimeInformationNotNullTest(string culture) + { + var userDataService = CreateUserDataService(culture); + IEnumerable userData = userDataService.GetUserData().ToList(); + Assert.Multiple(() => + { + Assert.IsNotNull(userData.Select(x => x.Name == "Server OS")); + Assert.IsNotNull(userData.Select(x => x.Name == "Server Framework")); + Assert.IsNotNull(userData.Select(x => x.Name == "Current Webserver")); + }); + } + + private UserDataService CreateUserDataService(string culture) + { + var localizationService = CreateILocalizationService(culture); + return new UserDataService(_umbracoVersion, localizationService); + } + + private ILocalizationService CreateILocalizationService(string culture) + { + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns(culture); + return localizationService.Object; + } + + private void CreateUmbracoVersion(int major, int minor, int patch) + { + var umbracoVersion = new Mock(); + var semVersion = new SemVersion(major, minor, patch); + umbracoVersion.Setup(x => x.SemanticVersion).Returns(semVersion); + _umbracoVersion = umbracoVersion.Object; + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs new file mode 100644 index 0000000000..42b9eb2ddc --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Dictionary; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Web.BackOffice.Controllers; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers +{ + [TestFixture] + public class ContentControllerTests + { + [Test] + public void Root_Node_With_Domains_Causes_No_Warning() + { + // Setup domain service + var domainServiceMock = new Mock(); + domainServiceMock.Setup(x => x.GetAssignedDomains(1060, It.IsAny())) + .Returns(new []{new UmbracoDomain("/", "da-dk"), new UmbracoDomain("/en", "en-us")}); + + // Create content, we need to specify and ID in order to be able to configure domain service + Content rootNode = new ContentBuilder() + .WithContentType(CreateContentType()) + .WithId(1060) + .AddContentCultureInfosCollection() + .AddCultureInfos() + .WithCultureIso("da-dk") + .Done() + .AddCultureInfos() + .WithCultureIso("en-us") + .Done() + .Done() + .Build(); + + var culturesPublished = new []{ "en-us", "da-dk" }; + var notifications = new SimpleNotificationModel(); + + ContentController contentController = CreateContentController(domainServiceMock.Object); + contentController.AddDomainWarnings(rootNode, culturesPublished, notifications); + + Assert.IsEmpty(notifications.Notifications); + } + + [Test] + public void Node_With_Single_Published_Culture_Causes_No_Warning() + { + var domainServiceMock = new Mock(); + domainServiceMock.Setup(x => x.GetAssignedDomains(It.IsAny(), It.IsAny())) + .Returns(Enumerable.Empty()); + + Content rootNode = new ContentBuilder() + .WithContentType(CreateContentType()) + .WithId(1060) + .AddContentCultureInfosCollection() + .AddCultureInfos() + .WithCultureIso("da-dk") + .Done() + .Done() + .Build(); + + var culturesPublished = new []{"da-dk" }; + var notifications = new SimpleNotificationModel(); + + ContentController contentController = CreateContentController(domainServiceMock.Object); + contentController.AddDomainWarnings(rootNode, culturesPublished, notifications); + + Assert.IsEmpty(notifications.Notifications); + } + + [Test] + public void Root_Node_Without_Domains_Causes_SingleWarning() + { + var domainServiceMock = new Mock(); + domainServiceMock.Setup(x => x.GetAssignedDomains(It.IsAny(), It.IsAny())) + .Returns(Enumerable.Empty()); + + Content rootNode = new ContentBuilder() + .WithContentType(CreateContentType()) + .WithId(1060) + .AddContentCultureInfosCollection() + .AddCultureInfos() + .WithCultureIso("da-dk") + .Done() + .AddCultureInfos() + .WithCultureIso("en-us") + .Done() + .Done() + .Build(); + + var culturesPublished = new []{ "en-us", "da-dk" }; + var notifications = new SimpleNotificationModel(); + + ContentController contentController = CreateContentController(domainServiceMock.Object); + contentController.AddDomainWarnings(rootNode, culturesPublished, notifications); + Assert.AreEqual(1, notifications.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + } + + [Test] + public void One_Warning_Per_Culture_Being_Published() + { + var domainServiceMock = new Mock(); + domainServiceMock.Setup(x => x.GetAssignedDomains(It.IsAny(), It.IsAny())) + .Returns(new []{new UmbracoDomain("/", "da-dk")}); + + + Content rootNode = new ContentBuilder() + .WithContentType(CreateContentType()) + .WithId(1060) + .AddContentCultureInfosCollection() + .AddCultureInfos() + .WithCultureIso("da-dk") + .Done() + .AddCultureInfos() + .WithCultureIso("en-us") + .Done() + .Done() + .Build(); + + var culturesPublished = new []{ "en-us", "da-dk", "nl-bk", "se-sv" }; + var notifications = new SimpleNotificationModel(); + + ContentController contentController = CreateContentController(domainServiceMock.Object); + contentController.AddDomainWarnings(rootNode, culturesPublished, notifications); + Assert.AreEqual(3, notifications.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + } + + [Test] + public void Ancestor_Domains_Counts() + { + var rootId = 1060; + var level1Id = 1061; + var level2Id = 1062; + var level3Id = 1063; + + var domainServiceMock = new Mock(); + domainServiceMock.Setup(x => x.GetAssignedDomains(rootId, It.IsAny())) + .Returns(new[] { new UmbracoDomain("/", "da-dk") }); + + domainServiceMock.Setup(x => x.GetAssignedDomains(level1Id, It.IsAny())) + .Returns(new[] { new UmbracoDomain("/en", "en-us") }); + + domainServiceMock.Setup(x => x.GetAssignedDomains(level2Id, It.IsAny())) + .Returns(new[] { new UmbracoDomain("/se", "se-sv"), new UmbracoDomain("/nl", "nl-bk") }); + + Content level3Node = new ContentBuilder() + .WithContentType(CreateContentType()) + .WithId(level3Id) + .WithPath($"-1,{rootId},{level1Id},{level2Id},{level3Id}") + .AddContentCultureInfosCollection() + .AddCultureInfos() + .WithCultureIso("da-dk") + .Done() + .AddCultureInfos() + .WithCultureIso("en-us") + .Done() + .AddCultureInfos() + .WithCultureIso("se-sv") + .Done() + .AddCultureInfos() + .WithCultureIso("nl-bk") + .Done() + .AddCultureInfos() + .WithCultureIso("de-de") + .Done() + .Done() + .Build(); + + var culturesPublished = new []{ "en-us", "da-dk", "nl-bk", "se-sv", "de-de" }; + + ContentController contentController = CreateContentController(domainServiceMock.Object); + var notifications = new SimpleNotificationModel(); + + contentController.AddDomainWarnings(level3Node, culturesPublished, notifications); + // We expect one error because all domains except "de-de" is registered somewhere in the ancestor path + Assert.AreEqual(1, notifications.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + } + + [Test] + public void Only_Warns_About_Cultures_Being_Published() + { + var domainServiceMock = new Mock(); + domainServiceMock.Setup(x => x.GetAssignedDomains(It.IsAny(), It.IsAny())) + .Returns(new []{new UmbracoDomain("/", "da-dk")}); + + Content rootNode = new ContentBuilder() + .WithContentType(CreateContentType()) + .WithId(1060) + .AddContentCultureInfosCollection() + .AddCultureInfos() + .WithCultureIso("da-dk") + .Done() + .AddCultureInfos() + .WithCultureIso("en-us") + .Done() + .AddCultureInfos() + .WithCultureIso("se-sv") + .Done() + .AddCultureInfos() + .WithCultureIso("de-de") + .Done() + .Done() + .Build(); + + var culturesPublished = new []{ "en-us", "se-sv" }; + var notifications = new SimpleNotificationModel(); + + ContentController contentController = CreateContentController(domainServiceMock.Object); + contentController.AddDomainWarnings(rootNode, culturesPublished, notifications); + + // We only get two errors, one for each culture being published, so no errors from previously published cultures. + Assert.AreEqual(2, notifications.Notifications.Count(x => x.NotificationType == NotificationStyle.Warning)); + } + + private ContentController CreateContentController(IDomainService domainService) + { + // We have to configure ILocalizedTextService to return a new string every time Localize is called + // Otherwise it won't add the notification because it skips dupes + var localizedTextServiceMock = new Mock(); + localizedTextServiceMock.Setup(x => x.Localize(It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(() => Guid.NewGuid().ToString()); + + var controller = new ContentController( + Mock.Of(), + NullLoggerFactory.Instance, + Mock.Of(), + Mock.Of(), + localizedTextServiceMock.Object, + new PropertyEditorCollection(new DataEditorCollection(() => null)), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + domainService, + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of(), + new ActionCollection(() => null), + Mock.Of(), + Mock.Of(), + Mock.Of(), + Mock.Of() + ); + + return controller; + } + + private IContentType CreateContentType() => + new ContentTypeBuilder().WithContentVariation(ContentVariation.Culture).Build(); + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index f9a0ff3792..0dfea020d8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -285,6 +285,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "memberTypeApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllTypes()) }, + { + "memberTypeQueryApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAllTypes()) + }, { "memberGroupApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllGroups()) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index beebb246d9..70678545d9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -398,7 +398,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers [HttpPost] public ActionResult> GetEmptyByAliases(ContentTypesByAliases contentTypesByAliases) { - // It's important to do this operation within a scope to reduce the amount of readlock queries. + // It's important to do this operation within a scope to reduce the amount of readlock queries. using var scope = _scopeProvider.CreateScope(autoComplete: true); var contentTypes = contentTypesByAliases.ContentTypeAliases.Select(alias => _contentTypeService.Get(alias)); return GetEmpties(contentTypes, contentTypesByAliases.ParentId).ToDictionary(x => x.ContentTypeAlias); @@ -879,7 +879,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers case ContentSaveAction.Publish: case ContentSaveAction.PublishNew: { - var publishStatus = PublishInternal(contentItem, defaultCulture, cultureForInvariantErrors, out wasCancelled, out var successfulCultures); + PublishResult publishStatus = PublishInternal(contentItem, defaultCulture, cultureForInvariantErrors, out wasCancelled, out var successfulCultures); + // Add warnings if domains are not set up correctly + AddDomainWarnings(publishStatus.Content, successfulCultures, globalNotifications); AddPublishStatusNotifications(new[] { publishStatus }, globalNotifications, notifications, successfulCultures); } break; @@ -896,6 +898,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } var publishStatus = PublishBranchInternal(contentItem, false, cultureForInvariantErrors, out wasCancelled, out var successfulCultures).ToList(); + AddDomainWarnings(publishStatus, successfulCultures, globalNotifications); AddPublishStatusNotifications(publishStatus, globalNotifications, notifications, successfulCultures); } break; @@ -1412,6 +1415,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent, culturesToPublish, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; successfulCultures = culturesToPublish; + return publishStatus; } else @@ -1425,6 +1429,73 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } + private void AddDomainWarnings(IEnumerable publishResults, string[] culturesPublished, + SimpleNotificationModel globalNotifications) + { + foreach (PublishResult publishResult in publishResults) + { + AddDomainWarnings(publishResult.Content, culturesPublished, globalNotifications); + } + } + + /// + /// Verifies that there's an appropriate domain setup for the published cultures + /// + /// + /// Adds a warning and logs a message if a node varies by culture, there's at least 1 culture already published, + /// and there's no domain added for the published cultures + /// + /// + /// + /// + internal void AddDomainWarnings(IContent persistedContent, string[] culturesPublished, SimpleNotificationModel globalNotifications) + { + // Don't try to verify if no cultures were published + if (culturesPublished is null) + { + return; + } + + var publishedCultures = GetPublishedCulturesFromAncestors(persistedContent).ToList(); + // If only a single culture is published we shouldn't have any routing issues + if (publishedCultures.Count < 2) + { + return; + } + + // If more than a single culture is published we need to verify that there's a domain registered for each published culture + var assignedDomains = _domainService.GetAssignedDomains(persistedContent.Id, true).ToHashSet(); + // We also have to check all of the ancestors, if any of those has the appropriate culture assigned we don't need to warn + foreach (var ancestorID in persistedContent.GetAncestorIds()) + { + assignedDomains.UnionWith(_domainService.GetAssignedDomains(ancestorID, true)); + } + + // No domains at all, add a warning, to add domains. + if (assignedDomains.Count == 0) + { + globalNotifications.AddWarningNotification( + _localizedTextService.Localize("auditTrails", "publish"), + _localizedTextService.Localize("speechBubbles", "publishWithNoDomains")); + + _logger.LogWarning("The root node {RootNodeName} was published with multiple cultures, but no domains are configured, this will cause routing and caching issues, please register domains for: {Cultures}", + persistedContent.Name, string.Join(", ", publishedCultures)); + return; + } + + // If there is some domains, verify that there's a domain for each of the published cultures + foreach (var culture in culturesPublished + .Where(culture => assignedDomains.Any(x => x.LanguageIsoCode.Equals(culture, StringComparison.OrdinalIgnoreCase)) is false)) + { + globalNotifications.AddWarningNotification( + _localizedTextService.Localize("auditTrails", "publish"), + _localizedTextService.Localize("speechBubbles", "publishWithMissingDomain", new []{culture})); + + _logger.LogWarning("The root node {RootNodeName} was published in culture {Culture}, but there's no domain configured for it, this will cause routing and caching issues, please register a domain for it", + persistedContent.Name, culture); + } + } + /// /// Validate if publishing is possible based on the mandatory language requirements /// @@ -1512,6 +1583,27 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return true; } + private IEnumerable GetPublishedCulturesFromAncestors(IContent content) + { + if (content.ParentId == -1) + { + return content.PublishedCultures; + } + + HashSet publishedCultures = new (); + publishedCultures.UnionWith(content.PublishedCultures); + + IEnumerable ancestorIds = content.GetAncestorIds(); + + foreach (var id in ancestorIds) + { + IEnumerable cultures = _contentService.GetById(id).PublishedCultures; + publishedCultures.UnionWith(cultures); + } + + return publishedCultures; + + } /// /// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index b3ef4b8665..364a24a8c3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -22,11 +23,10 @@ using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -47,12 +47,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IUserService _userService; private readonly IUmbracoMapper _umbracoMapper; private readonly IBackOfficeUserManager _backOfficeUserManager; - private readonly ILoggerFactory _loggerFactory; private readonly ILocalizedTextService _localizedTextService; private readonly AppCaches _appCaches; private readonly IShortStringHelper _shortStringHelper; private readonly IPasswordChanger _passwordChanger; + private readonly IUserDataService _userDataService; + [ActivatorUtilitiesConstructor] public CurrentUserController( MediaFileManager mediaFileManager, IOptionsSnapshot contentSettings, @@ -62,25 +63,57 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IUserService userService, IUmbracoMapper umbracoMapper, IBackOfficeUserManager backOfficeUserManager, + ILocalizedTextService localizedTextService, + AppCaches appCaches, + IShortStringHelper shortStringHelper, + IPasswordChanger passwordChanger, + IUserDataService userDataService) + { + _mediaFileManager = mediaFileManager; + _contentSettings = contentSettings.Value; + _hostingEnvironment = hostingEnvironment; + _imageUrlGenerator = imageUrlGenerator; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _userService = userService; + _umbracoMapper = umbracoMapper; + _backOfficeUserManager = backOfficeUserManager; + _localizedTextService = localizedTextService; + _appCaches = appCaches; + _shortStringHelper = shortStringHelper; + _passwordChanger = passwordChanger; + _userDataService = userDataService; + } + + [Obsolete("This constructor is obsolete and will be removed in v11, use constructor with all values")] + public CurrentUserController( + MediaFileManager mediaFileManager, + IOptions contentSettings, + IHostingEnvironment hostingEnvironment, + IImageUrlGenerator imageUrlGenerator, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IUserService userService, + IUmbracoMapper umbracoMapper, + IBackOfficeUserManager backOfficeUserManager, ILoggerFactory loggerFactory, ILocalizedTextService localizedTextService, AppCaches appCaches, IShortStringHelper shortStringHelper, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger) : this( + mediaFileManager, + contentSettings, + hostingEnvironment, + imageUrlGenerator, + backofficeSecurityAccessor, + userService, + umbracoMapper, + backOfficeUserManager, + localizedTextService, + appCaches, + shortStringHelper, + passwordChanger, + StaticServiceProvider.Instance.GetRequiredService()) { - _mediaFileManager = mediaFileManager; - _contentSettings = contentSettings.Value; - _hostingEnvironment = hostingEnvironment; - _imageUrlGenerator = imageUrlGenerator; - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _userService = userService; - _umbracoMapper = umbracoMapper; - _backOfficeUserManager = backOfficeUserManager; - _loggerFactory = loggerFactory; - _localizedTextService = localizedTextService; - _appCaches = appCaches; - _shortStringHelper = shortStringHelper; - _passwordChanger = passwordChanger; + } @@ -167,6 +200,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return userTours; } + public IEnumerable GetUserData() => _userDataService.GetUserData(); + /// /// When a user is invited and they click on the invitation link, they will be partially logged in /// where they can set their username/password diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs index 4af907bdfc..7c1f6f4187 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs @@ -182,6 +182,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// Returns all member types /// + [Obsolete("Use MemberTypeQueryController.GetAllTypes instead as it only requires AuthorizationPolicies.TreeAccessMembersOrMemberTypes and not both this and AuthorizationPolicies.TreeAccessMemberTypes")] [Authorize(Policy = AuthorizationPolicies.TreeAccessMembersOrMemberTypes)] public IEnumerable GetAllTypes() { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeQueryController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeQueryController.cs new file mode 100644 index 0000000000..1d15a6448a --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeQueryController.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + /// + /// An API controller used for dealing with member types + /// + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [Authorize(Policy = AuthorizationPolicies.TreeAccessMembersOrMemberTypes)] + public class MemberTypeQueryController : BackOfficeNotificationsController + { + private readonly IMemberTypeService _memberTypeService; + private readonly IUmbracoMapper _umbracoMapper; + + + public MemberTypeQueryController( + IMemberTypeService memberTypeService, + IUmbracoMapper umbracoMapper) + { + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + } + + /// + /// Returns all member types + /// + public IEnumerable GetAllTypes() => + _memberTypeService.GetAll() + .Select(_umbracoMapper.Map); + + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 677a589964..4d8da0641e 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -557,7 +557,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers // i.e. "Some Person" var toMailBoxAddress = new MailboxAddress(to.Name, to.Email); - var mailMessage = new EmailMessage(fromEmail, toMailBoxAddress.ToString(), emailSubject, emailBody, true); + var mailMessage = new EmailMessage(null /*use info from smtp settings*/, toMailBoxAddress.ToString(), emailSubject, emailBody, true); await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.UserInvite, true); } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs deleted file mode 100644 index fa5adf7aeb..0000000000 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoApplicationServicesCapture.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; - -namespace Umbraco.Cms.Web.Common.DependencyInjection -{ - /// - /// A registered to automatically capture application services - /// - internal class UmbracoApplicationServicesCapture : IStartupFilter - { - /// - public Action Configure(Action next) => - app => - { - StaticServiceProvider.Instance = app.ApplicationServices; - next(app); - }; - } -} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 62573cfc7b..6755159fc1 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -54,8 +54,14 @@ namespace Umbraco.Extensions .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) // We need to add CropWebProcessor before ResizeWebProcessor (until https://github.com/SixLabors/ImageSharp.Web/issues/182 is fixed) .RemoveProcessor() + .RemoveProcessor() + .RemoveProcessor() + .RemoveProcessor() .AddProcessor() - .AddProcessor(); + .AddProcessor() + .AddProcessor() + .AddProcessor() + .AddProcessor(); builder.Services.AddTransient, ImageSharpConfigurationOptions>(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index ef98553ba2..2d584f198e 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -123,10 +123,6 @@ namespace Umbraco.Extensions config, profiler); - // adds the umbraco startup filter which will call UseUmbraco early on before - // other start filters are applied (depending on the ordering of IStartupFilters in DI). - services.AddTransient(); - return new UmbracoBuilder(services, config, typeLoader, loggerFactory, profiler, appCaches, tempHostingEnvironment); } diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs index 4817956ef8..cc07b6bd28 100644 --- a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs @@ -362,7 +362,7 @@ namespace Umbraco.Cms.Web.Common.Security { // Store the userId for use after two factor check var userId = await UserManager.GetUserIdAsync(user); - await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, StoreTwoFactorInfo(userId, loginProvider)); + await Context.SignInAsync(TwoFactorAuthenticationType, StoreTwoFactorInfo(userId, loginProvider)); return SignInResult.TwoFactorRequired; } } @@ -372,7 +372,7 @@ namespace Umbraco.Cms.Web.Common.Security await Context.SignOutAsync(ExternalAuthenticationType); } if (loginProvider == null) - { + { await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") }); } else diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index fcd62febf4..537df5aab4 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -48,5 +48,4 @@ <_Parameter1>Umbraco.Tests.UnitTests - diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index a3be6996b1..23870d882f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -51,7 +51,6 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { [{ permissionToCheck: permission }, { nodeId: id }])), 'Failed to check permission for item ' + id); }, - getCurrentUserLinkedLogins: function () { return umbRequestHelper.resourcePromise( @@ -61,6 +60,14 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { "GetCurrentUserLinkedLogins")), 'Server call failed for getting current users linked logins'); }, + getUserData: function () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "GetUserData")), + 'Server call failed for getting current user data'); + }, saveTourStatus: function (tourStatus) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index bf02d9618e..e1d0fbe8ac 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -46,10 +46,10 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter, local return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( - "memberTypeApiBaseUrl", + "memberTypeQueryApiBaseUrl", "GetAllTypes")), 'Failed to retrieve data for member types id'); - }, + }, getById: function (id) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/platform.service.js b/src/Umbraco.Web.UI.Client/src/common/services/platform.service.js index 7834c2f781..acd9533151 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/platform.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/platform.service.js @@ -1,23 +1,84 @@ -(function() { - 'use strict'; +(function () { + 'use strict'; - function platformService() { + function platformService() { + const userAgentRules = [ + ['Aol', /AOLShield\/([0-9\._]+)/], + ['Edge', /Edge\/([0-9\._]+)/], + ['Edge-ios', /EdgiOS\/([0-9\._]+)/], + ['Yandexbrowser', /YaBrowser\/([0-9\._]+)/], + ['Kakaotalk', /KAKAOTALK\s([0-9\.]+)/], + ['Samsung', /SamsungBrowser\/([0-9\.]+)/], + ['Silk', /\bSilk\/([0-9._-]+)\b/], + ['MiUI', /MiuiBrowser\/([0-9\.]+)$/], + ['Beaker', /BeakerBrowser\/([0-9\.]+)/], + ['Edge-chromium', /EdgA?\/([0-9\.]+)/], + ['chromium-webview', /(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/], + ['Chrome', /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/], + ['PhantomJS', /PhantomJS\/([0-9\.]+)(:?\s|$)/], + ['Crios', /CriOS\/([0-9\.]+)(:?\s|$)/], + ['Firefox', /Firefox\/([0-9\.]+)(?:\s|$)/], + ['FxiOS', /FxiOS\/([0-9\.]+)/], + ['Opera-mini', /Opera Mini.*Version\/([0-9\.]+)/], + ['Opera', /Opera\/([0-9\.]+)(?:\s|$)/], + ['Opera', /OPR\/([0-9\.]+)(:?\s|$)/], + ['IE', /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/], + ['IE', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/], + ['IE', /MSIE\s(7\.0)/], + ['BB10', /BB10;\sTouch.*Version\/([0-9\.]+)/], + ['Android', /Android\s([0-9\.]+)/], + ['iOS', /Version\/([0-9\._]+).*Mobile.*Safari.*/], + ['Safari', /Version\/([0-9\._]+).*Safari/], + ['Facebook', /FB[AS]V\/([0-9\.]+)/], + ['Instagram', /Instagram\s([0-9\.]+)/], + ['iOS-webview', /AppleWebKit\/([0-9\.]+).*Mobile/], + ['iOS-webview', /AppleWebKit\/([0-9\.]+).*Gecko\)$/], + ['Curl', /^curl\/([0-9\.]+)$/] + ]; - function isMac() { - return navigator.platform.toUpperCase().indexOf('MAC')>=0; - } + function isMac() { + return navigator.platform.toUpperCase().indexOf('MAC') >= 0; + } - //////////// - - var service = { - isMac: isMac + function getBrowserInfo(){ + let data = matchUserAgent(navigator.userAgent); + console.log(data); + if(data){ + const test = data[1]; + return { + name : data[0], + version : test[1] }; + } + return null; + } - return service; + function matchUserAgent(ua) { + return (ua !== '' && userAgentRules.reduce ( + (matched, [browser, regex]) => { + if (matched) { + return matched; + } + const uaMatch = regex.exec(ua); + return !!uaMatch && [browser, uaMatch]; + }, + false + ) + ); + } - } + //////////// - angular.module('umbraco.services').factory('platformService', platformService); + var service = { + isMac: isMac, + getBrowserInfo : getBrowserInfo + }; + + return service; + + } + + angular.module('umbraco.services').factory('platformService', platformService); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js index 1a2f0735ce..aa10d5bf2f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/templatehelper.service.js @@ -16,7 +16,7 @@ partialViewName = parentId + "/" + partialViewName; } - return "@Html.Partial(\"" + partialViewName + "\")"; + return "@await Html.PartialAsync(\"" + partialViewName + "\")"; } function getQuerySnippet(queryExpression) { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less index 5c77a15ec7..7660e930a5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less @@ -101,9 +101,9 @@ } /* Make sure typography looks good */ -.umb-help-article h1, -.umb-help-article h2, -.umb-help-article h3, +.umb-help-article h1, +.umb-help-article h2, +.umb-help-article h3, .umb-help-article h4 { line-height: 1.3em; font-weight: bold; @@ -138,7 +138,7 @@ } .umb-help-section__title { - margin:0 0 10px; + margin:0 0 10px; } /* Help list */ @@ -147,10 +147,10 @@ list-style: none; margin-left: 0; margin-bottom: 0; - background: @white; + background: @white; border-radius: 3px; box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16); - + [data-element*="help-tours"] & { margin-bottom:5px; } @@ -166,9 +166,20 @@ border: 0 none; } + .umb-help-list-item:last-child { border-bottom: none; } +.umb-help-list-item__title-wrapper { + display:flex; + justify-content: space-between; + align-items: center; + + .umb-help-list-item { + flex: 1 0 auto; + width: auto; + } +} .umb-help-list-item__group-title i { margin-right:2px; @@ -185,7 +196,7 @@ .umb-help-list-item:hover, .umb-help-list-item:focus, .umb-help-list-item:active, -.umb-help-list-item > a:hover, +.umb-help-list-item > a:hover, .umb-help-list-item > a:focus, .umb-help-list-item > a:active { text-decoration: none; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js index c212a08951..5b9626c676 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -1,11 +1,10 @@ (function () { "use strict"; - function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) { + function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState, notificationsService, currentUserResource, platformService) { var vm = this; var evts = []; - vm.title = ""; vm.subtitle = "Umbraco version" + " " + Umbraco.Sys.ServerVariables.application.version; vm.section = $routeParams.section; @@ -13,16 +12,19 @@ vm.sectionName = ""; vm.customDashboard = null; vm.tours = []; + vm.systemInfoDisplay = false; vm.closeDrawer = closeDrawer; vm.startTour = startTour; vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage; vm.showTourButton = showTourButton; + vm.copyInformation = copyInformation; vm.showDocTypeTour = false; vm.docTypeTours = []; + vm.systemInfo = []; vm.nodeName = ''; - + function startTour(tour) { tourService.startTour(tour); closeDrawer(); @@ -34,7 +36,14 @@ localizationService.localize("general_help").then(function(data){ vm.title = data; }); - + currentUserResource.getUserData().then(function(systemInfo){ + vm.systemInfo = systemInfo; + let browserInfo = platformService.getBrowserInfo(); + if(browserInfo != null){ + vm.systemInfo.push({name :"Browser", data: browserInfo.name + " " + browserInfo.version}); + } + vm.systemInfo.push({name :"Browser OS", data: getPlatform()}); + }); tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; getTourGroupCompletedPercentage(); @@ -52,11 +61,11 @@ setSectionName(); userService.getCurrentUser().then(function (user) { - + vm.userType = user.userType; vm.userLang = user.locale; - vm.hasAccessToSettings = _.contains(user.allowedSections, 'settings'); + vm.hasAccessToSettings = _.contains(user.allowedSections, 'settings'); evts.push(eventsService.on("appState.treeState.changed", function (e, args) { handleSectionChange(); @@ -72,7 +81,7 @@ }); setDocTypeTour(editorState.getCurrent()); - + // check if a tour is running - if it is open the matching group var currentTour = tourService.getCurrentTour(); @@ -89,7 +98,7 @@ function handleSectionChange() { $timeout(function () { if (vm.section !== $routeParams.section || vm.tree !== $routeParams.tree) { - + vm.section = $routeParams.section; vm.tree = $routeParams.tree; @@ -107,7 +116,7 @@ vm.topics = topics; }); } - + var rq = {}; rq.section = vm.section; @@ -134,7 +143,7 @@ helpService.findVideos(rq).then(function (videos) { vm.videos = videos; }); - } + } } function setSectionName() { @@ -190,7 +199,7 @@ tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) { if (data && data.length > 0) { vm.docTypeTours = data; - var currentVariant = _.find(node.variants, (x) => x.active); + var currentVariant = _.find(node.variants, (x) => x.active); vm.nodeName = currentVariant.name; vm.showDocTypeTour = true; } @@ -198,7 +207,23 @@ } } } - + function copyInformation(){ + let copyText = "\n\n\n\nCategory | Data\n-- | --\n"; + vm.systemInfo.forEach(function (info){ + copyText += info.name + " | " + info.data + "\n"; + }); + copyText += "\n\n\n" + navigator.clipboard.writeText(copyText); + if(copyText != null){ + notificationsService.success("Copied!", "Your system information is now in your clipboard"); + } + else{ + notificationsService.error("Error", "Could not copy system information"); + } + } + function getPlatform() { + return window.navigator.platform; + } evts.push(eventsService.on("appState.tour.complete", function (event, tour) { tourService.getGroupedTours().then(function(groupedTours) { vm.tours = groupedTours; @@ -206,7 +231,7 @@ getTourGroupCompletedPercentage(); }); })); - + $scope.$on('$destroy', function () { for (var e in evts) { eventsService.unsubscribe(evts[e]); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 82a37e6efb..6f32e89988 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -149,6 +149,47 @@ + +
+ +
System Information
+
+
+ + +
+
+ + + + + + + + + +
CategoryData
{{info.name}}{{info.data}}
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index 60a4b9245a..9331e4227b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -109,7 +109,7 @@ } ] }; - + editorService.contentTypePicker(settingsTypePicker); }); @@ -179,7 +179,7 @@ }, close: () => editorService.close() }; - + editorService.filePicker(filePicker); }); @@ -206,26 +206,26 @@ }; vm.addStylesheetForBlock = function(block) { - localizationService.localize("blockEditor_headlineAddCustomStylesheet").then(localizedTitle => { + localizationService.localize("blockEditor_headlineAddCustomStylesheet").then(localizedTitle => { - const filePicker = { - title: localizedTitle, - isDialog: true, - filter: i => { - return !(i.name.indexOf(".css") !== -1); - }, - filterCssClass: "not-allowed", - select: node => { - const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); - block.stylesheet = "~/" + filepath; - editorService.close(); - }, - close: () => editorService.close() - }; + const filePicker = { + title: localizedTitle, + isDialog: true, + filter: i => { + return !(i.name.indexOf(".css") !== -1); + }, + filterCssClass: "not-allowed", + select: node => { + const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); + block.stylesheet = "~/" + filepath.replace("wwwroot/", ""); + editorService.close(); + }, + close: () => editorService.close() + }; - editorService.filePicker(filePicker); + editorService.staticFilePicker(filePicker); - }); + }); }; vm.requestRemoveStylesheetForBlock = function(block) { @@ -251,7 +251,7 @@ vm.addThumbnailForBlock = function(block) { localizationService.localize("blockEditor_headlineAddThumbnail").then(localizedTitle => { - + let allowedFileExtensions = ['jpg', 'jpeg', 'png', 'svg', 'webp', 'gif']; const thumbnailPicker = { @@ -269,7 +269,7 @@ }, close: () => editorService.close() }; - + editorService.staticFilePicker(thumbnailPicker); }); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/template-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/template-helper.spec.js index 316cfa7c59..69da0ce786 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/template-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/template-helper.spec.js @@ -26,28 +26,28 @@ describe('service: templateHelper', function () { it('should return the snippet for inserting a partial from the root', function () { var parentId = ""; var nodeName = "Footer.cshtml"; - var snippet = '@Html.Partial("Footer")'; + var snippet = '@await Html.PartialAsync("Footer")'; expect(templateHelper.getInsertPartialSnippet(parentId, nodeName)).toBe(snippet); }); it('should return the snippet for inserting a partial from a folder', function () { var parentId = "Folder"; var nodeName = "Footer.cshtml"; - var snippet = '@Html.Partial("Folder/Footer")'; + var snippet = '@await Html.PartialAsync("Folder/Footer")'; expect(templateHelper.getInsertPartialSnippet(parentId, nodeName)).toBe(snippet); }); it('should return the snippet for inserting a partial from a nested folder', function () { var parentId = "Folder/NestedFolder"; var nodeName = "Footer.cshtml"; - var snippet = '@Html.Partial("Folder/NestedFolder/Footer")'; + var snippet = '@await Html.PartialAsync("Folder/NestedFolder/Footer")'; expect(templateHelper.getInsertPartialSnippet(parentId, nodeName)).toBe(snippet); }); it('should return the snippet for inserting a partial from a folder with spaces in its name', function () { var parentId = "Folder with spaces"; var nodeName = "Footer.cshtml"; - var snippet = '@Html.Partial("Folder with spaces/Footer")'; + var snippet = '@await Html.PartialAsync("Folder with spaces/Footer")'; expect(templateHelper.getInsertPartialSnippet(parentId, nodeName)).toBe(snippet); }); diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 05c2caad27..3d62cd6be2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -1245,6 +1245,8 @@ Mange hilsner fra Umbraco robotten Kan ikke planlægge dokumentes udgivelse da det krævet '%0%' har en senere udgivelses dato end et ikke krævet sprog Afpubliceringsdatoen kan ikke ligge i fortiden Afpubliceringsdatoen kan ikke være før udgivelsesdatoen + Domæner er ikke konfigureret for en flersproget side, kontakt vensligst en administrator, se loggen for mere information + Der er ikke noget domæne konfigureret for %0%, kontakt vensligst en administrator, se loggen for mere information Tilføj style diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index dbb8a7a6b6..884aa7b682 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1441,6 +1441,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Invitation has been re-sent to %0% Document Type was exported to file An error occurred while exporting the Document Type + Domains are not configured for multilingual site, please contact an administrator, see log for more information + There is no domain configured for %0%, please contact an administrator, see log for more information Add style 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 06cc8de008..b5bd25446a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1470,6 +1470,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language The expire date cannot be in the past The expire date cannot be before the release date + Domains are not configured for multilingual site, please contact an administrator, see log for more information + There is no domain configured for %0%, please contact an administrator, see log for more information Add style