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