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/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 548387a675..590262946d 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -237,7 +237,7 @@ namespace Umbraco.Cms.Core.Routing // re-route await RouteRequestInternalAsync(builder); - + // return if we are redirect if (builder.IsRedirect()) { @@ -252,6 +252,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.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/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 e656e68193..67e69c0788 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.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.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.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index a982ed1744..58f3622e67 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -279,6 +279,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/CurrentUserController.cs b/src/Umbraco.Web.BackOffice/Controllers/CurrentUserController.cs index ba6ca36085..c4d6bac0fa 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,44 @@ 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, + IOptions contentSettings, + IHostingEnvironment hostingEnvironment, + IImageUrlGenerator imageUrlGenerator, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + 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, @@ -66,21 +98,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers 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 7e62e514f7..672495c7eb 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/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); });