diff --git a/build/V8-Docs-Pipeline.yml b/build/V8-Docs-Pipeline.yml new file mode 100644 index 0000000000..56e8fde60c --- /dev/null +++ b/build/V8-Docs-Pipeline.yml @@ -0,0 +1,52 @@ +############################################################## +## V8 CMS - .NET & AngularJS Doc sites ## +## Built on demand only, NO automatic PR/branch triggers ## +## ## +## This build pipeline has a webhook for sucessful ## +## builds, that sends the ZIP artifacts to our.umb to host ## +############################################################## + +# Name != name of pipeline but the build number format +# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/run-number?view=azure-devops&tabs=yaml + +# Build Pipeline triggers +# https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#ci-triggers +trigger: none +pr: none + +# Variables & their default values +variables: + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +# VM to run the build on & it's installed software +# https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops +# https://github.com/actions/virtual-environments/blob/master/images/win/Windows2019-Readme.md +pool: + vmImage: 'windows-2019' + +jobs: + - job: buildDocs + displayName: 'Build static docs site as ZIPs' + steps: + + - task: PowerShell@2 + displayName: 'Prep build tool, build C# & JS Docs' + inputs: + targetType: 'inline' + script: | + $uenv=./build.ps1 -get -doc + $uenv.SandboxNode() + $uenv.CompileBelle() + $uenv.PrepareAngularDocs() + $nugetsourceUmbraco = "https://api.nuget.org/v3/index.json" + $uenv.PrepareCSharpDocs() + $uenv.RestoreNode() + errorActionPreference: 'continue' + workingDirectory: 'build' + + - task: PublishPipelineArtifact@1 + inputs: + targetPath: '$(Build.Repository.LocalPath)\build.out\' + artifact: 'docs' + publishLocation: 'pipeline' \ No newline at end of file diff --git a/build/build.ps1 b/build/build.ps1 index 3ba347a6dc..c2c5bdd232 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -438,14 +438,11 @@ Write-Host "Prepare C# Documentation" $src = "$($this.SolutionRoot)\src" - $tmp = $this.BuildTemp - $out = $this.BuildOutput + $tmp = $this.BuildTemp + $out = $this.BuildOutput $DocFxJson = Join-Path -Path $src "\ApiDocs\docfx.json" $DocFxSiteOutput = Join-Path -Path $tmp "\_site\*.*" - - #restore nuget packages - $this.RestoreNuGet() # run DocFx $DocFx = $this.BuildEnv.DocFx @@ -463,17 +460,22 @@ $src = "$($this.SolutionRoot)\src" $out = $this.BuildOutput + # Check if the solution has been built + if (!(Test-Path "$src\Umbraco.Web.UI.Client\node_modules")) {throw "Umbraco needs to be built before generating the Angular Docs"} + "Moving to Umbraco.Web.UI.Docs folder" - cd ..\src\Umbraco.Web.UI.Docs + cd $src\Umbraco.Web.UI.Docs "Generating the docs and waiting before executing the next commands" & npm install & npx gulp docs + Pop-Location + # change baseUrl $BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" $IndexPath = "./api/index.html" - (Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath + (Get-Content $IndexPath).replace('origin + location.href.substr(origin.length).replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath # zip it & $this.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Docs\api\*.*" diff --git a/src/Umbraco.Configuration/Legacy/SmtpSettings.cs b/src/Umbraco.Configuration/Legacy/SmtpSettings.cs index 7e3ff80690..dce3d85840 100644 --- a/src/Umbraco.Configuration/Legacy/SmtpSettings.cs +++ b/src/Umbraco.Configuration/Legacy/SmtpSettings.cs @@ -1,3 +1,4 @@ +using System.Net.Mail; using Umbraco.Core.Configuration; namespace Umbraco.Configuration @@ -8,5 +9,10 @@ namespace Umbraco.Configuration public string Host { get; set; } public int Port { get; set; } public string PickupDirectoryLocation { get; set; } + public SmtpDeliveryMethod DeliveryMethod { get; set; } + + public string Username { get; set; } + + public string Password { get; set; } } } diff --git a/src/Umbraco.Configuration/Models/GlobalSettings.cs b/src/Umbraco.Configuration/Models/GlobalSettings.cs index 4b30813bd5..e4995cfb0b 100644 --- a/src/Umbraco.Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Configuration/Models/GlobalSettings.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Net.Mail; using Microsoft.Extensions.Configuration; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -72,10 +73,10 @@ namespace Umbraco.Configuration.Models _configuration.GetValue(Prefix + "NoNodesViewPath", "~/config/splashes/NoNodes.cshtml"); public bool IsSmtpServerConfigured => - _configuration.GetSection(Constants.Configuration.ConfigPrefix + "Smtp")?.GetChildren().Any() ?? false; + _configuration.GetSection(Constants.Configuration.ConfigGlobalPrefix + "Smtp")?.GetChildren().Any() ?? false; public ISmtpSettings SmtpSettings => - new SmtpSettingsImpl(_configuration.GetSection(Constants.Configuration.ConfigPrefix + "Smtp")); + new SmtpSettingsImpl(_configuration.GetSection(Constants.Configuration.ConfigGlobalPrefix + "Smtp")); private class SmtpSettingsImpl : ISmtpSettings { @@ -90,6 +91,11 @@ namespace Umbraco.Configuration.Models public string Host => _configurationSection.GetValue("Host"); public int Port => _configurationSection.GetValue("Port"); public string PickupDirectoryLocation => _configurationSection.GetValue("PickupDirectoryLocation"); + public SmtpDeliveryMethod DeliveryMethod => _configurationSection.GetValue("DeliveryMethod"); + + public string Username => _configurationSection.GetValue("Username"); + + public string Password => _configurationSection.GetValue("Password"); } } } diff --git a/src/Umbraco.Core/Configuration/ISmtpSettings.cs b/src/Umbraco.Core/Configuration/ISmtpSettings.cs index c2fb4b2dbe..ea42ae5567 100644 --- a/src/Umbraco.Core/Configuration/ISmtpSettings.cs +++ b/src/Umbraco.Core/Configuration/ISmtpSettings.cs @@ -1,3 +1,5 @@ +using System.Net.Mail; + namespace Umbraco.Core.Configuration { public interface ISmtpSettings @@ -6,5 +8,8 @@ namespace Umbraco.Core.Configuration string Host { get; } int Port{ get; } string PickupDirectoryLocation { get; } + SmtpDeliveryMethod DeliveryMethod { get; } + string Username { get; } + string Password { get; } } } diff --git a/src/Umbraco.Core/FactoryExtensions.cs b/src/Umbraco.Core/FactoryExtensions.cs index 8514525417..8ae2f76af3 100644 --- a/src/Umbraco.Core/FactoryExtensions.cs +++ b/src/Umbraco.Core/FactoryExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Umbraco.Core.Composing; @@ -77,15 +78,28 @@ namespace Umbraco.Core var ctorParameters = ctor.GetParameters(); var ctorArgs = new object[ctorParameters.Length]; + var availableArgs = new List(args); var i = 0; foreach (var parameter in ctorParameters) { // no! IsInstanceOfType is not ok here // ReSharper disable once UseMethodIsInstanceOfType - var arg = args?.FirstOrDefault(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); - ctorArgs[i++] = arg ?? factory.GetInstance(parameter.ParameterType); + var idx = availableArgs.FindIndex(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); + if(idx >= 0) + { + // Found a suitable supplied argument + ctorArgs[i++] = availableArgs[idx]; + + // A supplied argument can be used at most once + availableArgs.RemoveAt(idx); + } + else + { + // None of the provided arguments is suitable: get an instance from the factory + ctorArgs[i++] = factory.GetInstance(parameter.ParameterType); + } } return ctor.Invoke(ctorArgs); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs index 225e29a8a1..f27feba8cf 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs @@ -16,6 +16,11 @@ /// public string Culture { get; set; } + /// + /// When dealing with content variants, this is the segment for the variant + /// + public string Segment { get; set; } + /// /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. /// diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 6990a7f7da..7a4fc83253 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -25,6 +25,7 @@ namespace Umbraco.Core.Models /// /// Gets a value indicating whether the content is published. /// + /// The property tells you which version of the content is currently published. bool Published { get; set; } PublishedState PublishedState { get; set; } @@ -32,10 +33,11 @@ namespace Umbraco.Core.Models /// /// Gets a value indicating whether the content has been edited. /// + /// Will return `true` once unpublished edits have been made after the version with has been published. bool Edited { get; set; } /// - /// Gets the published version identifier. + /// Gets the version identifier for the currently published version of the content. /// int PublishedVersionId { get; set; } @@ -129,6 +131,6 @@ namespace Umbraco.Core.Models /// /// IContent DeepCloneWithResetIdentities(); - + } } diff --git a/src/Umbraco.Core/Models/RelationTypeExtensions.cs b/src/Umbraco.Core/Models/RelationTypeExtensions.cs new file mode 100644 index 0000000000..4d9d6856cb --- /dev/null +++ b/src/Umbraco.Core/Models/RelationTypeExtensions.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Models +{ + public static class RelationTypeExtensions + { + public static bool IsSystemRelationType(this IRelationType relationType) => + relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + } +} diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs index 9d4c76eea1..094c6b24ff 100644 --- a/src/Umbraco.Core/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using System.Threading; namespace Umbraco.Web.Models.Trees { @@ -28,12 +29,15 @@ namespace Umbraco.Web.Models.Trees Name = name; } - public MenuItem(string alias, ILocalizedTextService textService) : this() { + var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); + values.TryGetValue($"visuallyHiddenTexts/{alias}_description", out var textDescription); + Alias = alias; Name = textService.Localize($"actions/{Alias}"); + TextDescription = textDescription; } /// @@ -74,6 +78,9 @@ namespace Umbraco.Web.Models.Trees [Required] public string Alias { get; set; } + [DataMember(Name = "textDescription")] + public string TextDescription { get; set; } + /// /// Ensures a menu separator will exist before this menu item /// diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index 546fa0390f..1410575fdb 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading; using Umbraco.Core.Services; using Umbraco.Web.Actions; @@ -50,14 +51,17 @@ namespace Umbraco.Web.Models.Trees var item = _actionCollection.GetAction(); if (item == null) return null; + var values = textService.GetAllStoredValues(Thread.CurrentThread.CurrentUICulture); + values.TryGetValue($"visuallyHiddenTexts/{item.Alias}Description", out var textDescription); + var menuItem = new MenuItem(item, textService.Localize($"actions/{item.Alias}")) { SeparatorBefore = hasSeparator, - OpensDialog = opensDialog + OpensDialog = opensDialog, + TextDescription = textDescription, }; return menuItem; } - } } diff --git a/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs b/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs index a64bf15b7f..c21b09523d 100644 --- a/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Infrastructure/Models/BackOfficeTourStep.cs @@ -29,5 +29,7 @@ namespace Umbraco.Web.Models public string EventElement { get; set; } [DataMember(Name = "customProperties")] public JObject CustomProperties { get; set; } + [DataMember(Name = "skipStepIfVisible")] + public string SkipStepIfVisible { get; set; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs index 49b0e15e6b..1d31f8a0de 100644 --- a/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Infrastructure/Models/ContentEditing/RelationTypeDisplay.cs @@ -13,6 +13,9 @@ namespace Umbraco.Web.Models.ContentEditing Notifications = new List(); } + [DataMember(Name = "isSystemRelationType")] + public bool IsSystemRelationType { get; set; } + /// /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) /// diff --git a/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs index d6ec4fd969..836b04ca69 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/RelationMapDefinition.cs @@ -39,6 +39,8 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); target.Path = "-1," + source.Id; + target.IsSystemRelationType = source.IsSystemRelationType(); + // Set the "friendly" and entity names for the parent and child object types if (source.ParentObjectType.HasValue) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 70490af1a3..360c2be9fb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.PropertyEditors private readonly IContentTypeService _contentTypeService; private readonly IIOHelper _ioHelper; - internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; + public const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; public NestedContentPropertyEditor( ILogger logger, diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 19d3716e1c..34d69c2cd5 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -320,6 +320,7 @@ namespace Umbraco.Core.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); composition.RegisterUnique(); diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index a52fc652bb..3f931d6358 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -3,9 +3,12 @@ using System.Data; using System.Data.SqlClient; using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Umbraco.Core.Configuration; +using System.Web; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -17,9 +20,10 @@ namespace Umbraco.Core.Runtime public class SqlMainDomLock : IMainDomLock { private string _lockId; - private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; + private readonly IHostingEnvironment _hostingEnvironment; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); @@ -28,17 +32,20 @@ namespace Umbraco.Core.Runtime private bool _hasError; private object _locker = new object(); - public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator) + public SqlMainDomLock(ILogger logger, IGlobalSettings globalSettings, IConnectionStrings connectionStrings, IDbProviderFactoryCreator dbProviderFactoryCreator, IHostingEnvironment hostingEnvironment) { // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; + _hostingEnvironment = hostingEnvironment; _dbFactory = new UmbracoDatabaseFactory(_logger, globalSettings, connectionStrings, Constants.System.UmbracoConnectionName, new Lazy(() => new MapperCollection(Enumerable.Empty())), dbProviderFactoryCreator); + + MainDomKey = MainDomKeyPrefix + "-" + (NetworkHelper.MachineName + MainDom.GetMainDomId(_hostingEnvironment)).GenerateHash(); } public async Task AcquireLockAsync(int millisecondsTimeout) @@ -128,6 +135,16 @@ namespace Umbraco.Core.Runtime } + /// + /// Returns the keyvalue table key for the current server/app + /// + /// + /// The key is the the normal MainDomId which takes into account the AppDomainAppId and the physical file path of the app and this is + /// combined with the current machine name. The machine name is required because the default semaphore lock is machine wide so it implicitly + /// takes into account machine name whereas this needs to be explicitly per machine. + /// + private string MainDomKey { get; } + private void ListeningLoop() { while (true) diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 7e39894aa3..9cd911e8d4 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -508,6 +508,16 @@ namespace Umbraco.Core.Services.Implement // delete content DeleteItemsOfTypes(descendantsAndSelf.Select(x => x.Id)); + + // Next find all other document types that have a reference to this content type + var referenceToAllowedContentTypes = GetAll().Where(q => q.AllowedContentTypes.Any(p=>p.Id.Value==item.Id)); + foreach (var reference in referenceToAllowedContentTypes) + { + reference.AllowedContentTypes = reference.AllowedContentTypes.Where(p => p.Id.Value != item.Id); + var changedRef = new List>() { new ContentTypeChange(reference, ContentTypeChangeTypes.RefreshMain) }; + // Fire change event + OnChanged(scope, changedRef.ToEventArgs()); + } // finally delete the content type // - recursively deletes all descendants @@ -515,7 +525,7 @@ namespace Umbraco.Core.Services.Implement // (contents of any descendant type have been deleted but // contents of any composed (impacted) type remain but // need to have their property data cleared) - Repository.Delete(item); + Repository.Delete(item); //... var changes = descendantsAndSelf.Select(x => new ContentTypeChange(x, ContentTypeChangeTypes.Remove)) diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index db3ae1bc25..8da86df6e8 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Umbraco.Core/EmailSender.cs b/src/Umbraco.Infrastructure/Users/EmailSender.cs similarity index 55% rename from src/Umbraco.Core/EmailSender.cs rename to src/Umbraco.Infrastructure/Users/EmailSender.cs index 5cfdd765bc..9a2f425021 100644 --- a/src/Umbraco.Core/EmailSender.cs +++ b/src/Umbraco.Infrastructure/Users/EmailSender.cs @@ -1,9 +1,12 @@ using System; +using System.Linq; using System.Net.Mail; using System.Threading.Tasks; -using Umbraco.Core.Composing; +using MimeKit; +using MimeKit.Text; using Umbraco.Core.Configuration; using Umbraco.Core.Events; +using SmtpClient = MailKit.Net.Smtp.SmtpClient; namespace Umbraco.Core { @@ -21,7 +24,7 @@ namespace Umbraco.Core { } - internal EmailSender(IGlobalSettings globalSettings, bool enableEvents) + public EmailSender(IGlobalSettings globalSettings, bool enableEvents) { _globalSettings = globalSettings; _enableEvents = enableEvents; @@ -45,7 +48,17 @@ namespace Umbraco.Core { using (var client = new SmtpClient()) { - client.Send(message); + + client.Connect(_globalSettings.SmtpSettings.Host, _globalSettings.SmtpSettings.Port); + + if (!(_globalSettings.SmtpSettings.Username is null && + _globalSettings.SmtpSettings.Password is null)) + { + client.Authenticate(_globalSettings.SmtpSettings.Username, _globalSettings.SmtpSettings.Password); + } + + client.Send(ConstructEmailMessage(message)); + client.Disconnect(true); } } } @@ -65,14 +78,25 @@ namespace Umbraco.Core { using (var client = new SmtpClient()) { - if (client.DeliveryMethod == SmtpDeliveryMethod.Network) + await client.ConnectAsync(_globalSettings.SmtpSettings.Host, _globalSettings.SmtpSettings.Port); + + if (!(_globalSettings.SmtpSettings.Username is null && + _globalSettings.SmtpSettings.Password is null)) { - await client.SendMailAsync(message); + await client.AuthenticateAsync(_globalSettings.SmtpSettings.Username, _globalSettings.SmtpSettings.Password); + } + + var mailMessage = ConstructEmailMessage(message); + if (_globalSettings.SmtpSettings.DeliveryMethod == SmtpDeliveryMethod.Network) + { + await client.SendAsync(mailMessage); } else { - client.Send(message); + client.Send(mailMessage); } + + await client.DisconnectAsync(true); } } } @@ -83,7 +107,7 @@ namespace Umbraco.Core /// /// We assume this is possible if either an event handler is registered or an smtp server is configured /// - internal static bool CanSendRequiredEmail(IGlobalSettings globalSettings) => EventHandlerRegistered || globalSettings.IsSmtpServerConfigured; + public static bool CanSendRequiredEmail(IGlobalSettings globalSettings) => EventHandlerRegistered || globalSettings.IsSmtpServerConfigured; /// /// returns true if an event handler has been registered @@ -103,5 +127,22 @@ namespace Umbraco.Core var handler = SendEmail; if (handler != null) handler(null, e); } + + private MimeMessage ConstructEmailMessage(MailMessage mailMessage) + { + var fromEmail = mailMessage.From?.Address; + if(string.IsNullOrEmpty(fromEmail)) + fromEmail = _globalSettings.SmtpSettings.From; + + var messageToSend = new MimeMessage + { + Subject = mailMessage.Subject, + From = { new MailboxAddress(fromEmail)}, + Body = new TextPart(mailMessage.IsBodyHtml ? TextFormat.Html : TextFormat.Plain) { Text = mailMessage.Body } + }; + messageToSend.To.AddRange(mailMessage.To.Select(x=>new MailboxAddress(x.Address))); + + return messageToSend; + } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs index adff28a2ba..ec5424ad9a 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentNestedData.cs @@ -9,15 +9,28 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// internal class ContentNestedData { - [JsonProperty("properties")] + //dont serialize empty properties + [JsonProperty("pd")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary PropertyData { get; set; } - [JsonProperty("cultureData")] + [JsonProperty("cd")] [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] public Dictionary CultureData { get; set; } - [JsonProperty("urlSegment")] + [JsonProperty("us")] public string UrlSegment { get; set; } + + //Legacy properties used to deserialize existing nucache db entries + [JsonProperty("properties")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + private Dictionary LegacyPropertyData { set { PropertyData = value; } } + + [JsonProperty("cultureData")] + [JsonConverter(typeof(CaseInsensitiveDictionaryConverter))] + private Dictionary LegacyCultureData { set { CultureData = value; } } + + [JsonProperty("urlSegment")] + private string LegacyUrlSegment { set { UrlSegment = value; } } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs index 2521d42744..b59e8c403c 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/CultureVariation.cs @@ -8,16 +8,29 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource /// public class CultureVariation { - [JsonProperty("name")] + [JsonProperty("nm")] public string Name { get; set; } - [JsonProperty("urlSegment")] + [JsonProperty("us")] public string UrlSegment { get; set; } - [JsonProperty("date")] + [JsonProperty("dt")] public DateTime Date { get; set; } - [JsonProperty("isDraft")] + [JsonProperty("isd")] public bool IsDraft { get; set; } + + //Legacy properties used to deserialize existing nucache db entries + [JsonProperty("name")] + private string LegacyName { set { Name = value; } } + + [JsonProperty("urlSegment")] + private string LegacyUrlSegment { set { UrlSegment = value; } } + + [JsonProperty("date")] + private DateTime LegacyDate { set { Date = value; } } + + [JsonProperty("isDraft")] + private bool LegacyIsDraft { set { IsDraft = value; } } } } diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs index 9ab9401d71..cf7ab95360 100644 --- a/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs +++ b/src/Umbraco.PublishedCache.NuCache/DataSource/PropertyData.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Newtonsoft.Json; namespace Umbraco.Web.PublishedCache.NuCache.DataSource @@ -8,21 +9,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private string _culture; private string _segment; - [JsonProperty("culture")] + [DefaultValue("")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "c")] public string Culture { get => _culture; set => _culture = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } - [JsonProperty("seg")] + [DefaultValue("")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, PropertyName = "s")] public string Segment { get => _segment; set => _segment = value ?? throw new ArgumentNullException(nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null } - [JsonProperty("val")] + [JsonProperty("v")] public object Value { get; set; } + + + //Legacy properties used to deserialize existing nucache db entries + [JsonProperty("culture")] + private string LegacyCulture + { + set => Culture = value; + } + + [JsonProperty("seg")] + private string LegacySegment + { + set => Segment = value; + } + + [JsonProperty("val")] + private object LegacyValue + { + set => Value = value; + } } } diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index e5f3ffaa6a..815fe0a168 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -900,11 +900,12 @@ namespace Umbraco.Web.PublishedCache.NuCache // we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which // they require. - // These cannot currently be run side by side in parallel, due to the monitors need to be exits my the same thread that enter them. + // These can be run side by side in parallel. using (_contentStore.GetScopedWriteLock(_scopeProvider)) { NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); } + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) { NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress.json b/src/Umbraco.Tests.AcceptanceTest/cypress.json index 051bf4a871..33978211ed 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress.json +++ b/src/Umbraco.Tests.AcceptanceTest/cypress.json @@ -6,5 +6,6 @@ "username": "", "password": "" }, - "supportFile": "cypress/support/index.ts" + "supportFile": "cypress/support/index.ts", + "videoUploadOnPasses" : false } diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts index 17b7bb6805..49bcf94943 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -6,7 +6,7 @@ context('Languages', () => { }); it('Add language', () => { - const name = "Neddersass’sch (Nedderlannen)"; // Must be an option in the select box + const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box cy.umbracoEnsureLanguageNameNotExists(name); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts index bbabbcb4bf..6871db7ffe 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -1,4 +1,6 @@ /// +import {DocumentTypeBuilder, TemplateBuilder} from "umbraco-cypress-testhelpers"; + context('Templates', () => { beforeEach(() => { @@ -6,28 +8,50 @@ context('Templates', () => { }); it('Create template', () => { - const name = "Test template"; + const name = "Test template"; - cy.umbracoEnsureTemplateNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); - cy.umbracoSection('settings'); - cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); - cy.umbracoTreeItem("settings", ["Templates"]).rightclick(); + cy.umbracoTreeItem("settings", ["Templates"]).rightclick(); - cy.umbracoContextMenuAction("action-create").click(); + cy.umbracoContextMenuAction("action-create").click(); - //Type name - cy.umbracoEditorHeaderName(name); + //Type name + cy.umbracoEditorHeaderName(name); - //Save - cy.get('.btn-success').click(); + //Save + cy.get("form[name='contentForm']").submit(); - //Assert - cy.umbracoSuccessNotification().should('be.visible'); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); - //Clean up - cy.umbracoEnsureTemplateNameNotExists(name); + //Clean up + cy.umbracoEnsureTemplateNameNotExists(name); }); + it('Delete template', () => { + const name = "Test template"; + cy.umbracoEnsureTemplateNameNotExists(name); + + const template = new TemplateBuilder() + .withName(name) + .build(); + + cy.saveTemplate(template); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Templates", name]).rightclick(); + cy.umbracoContextMenuAction("action-delete").click(); + + cy.umbracoButtonByLabelKey("general_ok").click(); + + cy.contains(name).should('not.exist'); + + cy.umbracoEnsureTemplateNameNotExists(name); + }); }); diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index daa1c424bb..ad125d090a 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -6,8 +6,8 @@ "devDependencies": { "cross-env": "^7.0.2", "ncp": "^2.0.0", - "cypress": "^4.5.0", - "umbraco-cypress-testhelpers": "1.0.0-beta-38" + "cypress": "^4.6.0", + "umbraco-cypress-testhelpers": "1.0.0-beta-39" }, "dependencies": { "typescript": "^3.9.2" diff --git a/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs b/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs index 344d7bcf87..bd85807203 100644 --- a/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs +++ b/src/Umbraco.Tests.Common/Builders/SmtpSettingsBuilder.cs @@ -1,4 +1,6 @@ -using Umbraco.Core.Configuration; +using System.Net.Mail; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; namespace Umbraco.Tests.Common.Builders { @@ -16,6 +18,9 @@ namespace Umbraco.Tests.Common.Builders private string _host; private int? _port; private string _pickupDirectoryLocation; + private SmtpDeliveryMethod? _deliveryMethod; + private string _username; + private string _password; public SmtpSettingsBuilder(TParent parentBuilder) : base(parentBuilder) { @@ -33,24 +38,45 @@ namespace Umbraco.Tests.Common.Builders return this; } + public SmtpSettingsBuilder WithUsername(string username) + { + _username = username; + return this; + } + public SmtpSettingsBuilder WithPost(int port) { _port = port; return this; } + public SmtpSettingsBuilder WithPassword(string password) + { + _password = password; + return this; + } + public SmtpSettingsBuilder WithPickupDirectoryLocation(string pickupDirectoryLocation) { _pickupDirectoryLocation = pickupDirectoryLocation; return this; } + public SmtpSettingsBuilder WithDeliveryMethod(SmtpDeliveryMethod deliveryMethod) + { + _deliveryMethod = deliveryMethod; + return this; + } + public override ISmtpSettings Build() { var from = _from ?? null; var host = _host ?? null; var port = _port ?? 25; var pickupDirectoryLocation = _pickupDirectoryLocation ?? null; + var deliveryMethod = _deliveryMethod ?? SmtpDeliveryMethod.Network; + var username = _username ?? null; + var password = _password ?? null; return new TestSmtpSettings() { @@ -58,6 +84,9 @@ namespace Umbraco.Tests.Common.Builders Host = host, Port = port, PickupDirectoryLocation = pickupDirectoryLocation, + DeliveryMethod = deliveryMethod, + Username = username, + Password = password, }; } @@ -67,6 +96,9 @@ namespace Umbraco.Tests.Common.Builders public string Host { get; set; } public int Port { get; set; } public string PickupDirectoryLocation { get; set; } + public SmtpDeliveryMethod DeliveryMethod { get; set; } + public string Username { get; set; } + public string Password { get; set; } } } } diff --git a/src/Umbraco.Tests.Common/TestClone.cs b/src/Umbraco.Tests.Common/TestClone.cs new file mode 100644 index 0000000000..5fd0aa30e2 --- /dev/null +++ b/src/Umbraco.Tests.Common/TestClone.cs @@ -0,0 +1,77 @@ +using System; +using Umbraco.Core.Models; + +namespace Umbraco.Tests.Common +{ + public class TestClone : IDeepCloneable, IEquatable + { + public TestClone(Guid id) + { + Id = id; + IsClone = true; + } + + public TestClone() + { + Id = Guid.NewGuid(); + } + + public Guid Id { get; } + public bool IsClone { get; } + + public object DeepClone() + { + return new TestClone(Id); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(TestClone other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((TestClone)obj); + } + + /// + /// Serves as the default hash function. + /// + /// + /// A hash code for the current object. + /// + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public static bool operator ==(TestClone left, TestClone right) + { + return Equals(left, right); + } + + public static bool operator !=(TestClone left, TestClone right) + { + return Equals(left, right) == false; + } + } +} diff --git a/src/Umbraco.Tests/CoreThings/AttemptTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/AttemptTests.cs similarity index 92% rename from src/Umbraco.Tests/CoreThings/AttemptTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/AttemptTests.cs index a004095243..9a16c5b10f 100644 --- a/src/Umbraco.Tests/CoreThings/AttemptTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/AttemptTests.cs @@ -1,12 +1,11 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class AttemptTests { - [Test] public void AttemptIf() { diff --git a/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ClaimsIdentityExtensionsTests.cs similarity index 96% rename from src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/ClaimsIdentityExtensionsTests.cs index 4f728c3861..76f928ca46 100644 --- a/src/Umbraco.Tests/CoreThings/ClaimsIdentityExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ClaimsIdentityExtensionsTests.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Security.Claims; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Web; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { public class ClaimsIdentityExtensionsTests { diff --git a/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/DeepCloneableListTests.cs similarity index 53% rename from src/Umbraco.Tests/Collections/DeepCloneableListTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/DeepCloneableListTests.cs index 37e5b2081d..bcfe142d8d 100644 --- a/src/Umbraco.Tests/Collections/DeepCloneableListTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/DeepCloneableListTests.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; using NUnit.Framework; using Umbraco.Core.Collections; -using Umbraco.Core.Models; +using Umbraco.Tests.Common; -namespace Umbraco.Tests.Collections +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Collections { [TestFixture] public class DeepCloneableListTests @@ -85,7 +81,7 @@ namespace Umbraco.Tests.Collections list.Add(new TestClone()); list.Add(new TestClone()); - var cloned = (DeepCloneableList)list.DeepClone(); + var cloned = (DeepCloneableList) list.DeepClone(); //Test that each item in the sequence is equal - based on the equality comparer of TestClone (i.e. it's ID) Assert.IsTrue(list.SequenceEqual(cloned)); @@ -97,77 +93,5 @@ namespace Umbraco.Tests.Collections Assert.AreNotSame(item, clone); } } - - public class TestClone : IDeepCloneable, IEquatable - { - public TestClone(Guid id) - { - Id = id; - IsClone = true; - } - - public TestClone() - { - Id = Guid.NewGuid(); - } - - public Guid Id { get; private set; } - public bool IsClone { get; private set; } - - public object DeepClone() - { - return new TestClone(Id); - } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// - /// true if the current object is equal to the parameter; otherwise, false. - /// - /// An object to compare with this object. - public bool Equals(TestClone other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return Id.Equals(other.Id); - } - - /// - /// Determines whether the specified object is equal to the current object. - /// - /// - /// true if the specified object is equal to the current object; otherwise, false. - /// - /// The object to compare with the current object. - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((TestClone)obj); - } - - /// - /// Serves as the default hash function. - /// - /// - /// A hash code for the current object. - /// - public override int GetHashCode() - { - return Id.GetHashCode(); - } - - public static bool operator ==(TestClone left, TestClone right) - { - return Equals(left, right); - } - - public static bool operator !=(TestClone left, TestClone right) - { - return Equals(left, right) == false; - } - } } } diff --git a/src/Umbraco.Tests/Collections/OrderedHashSetTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/OrderedHashSetTests.cs similarity index 87% rename from src/Umbraco.Tests/Collections/OrderedHashSetTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/OrderedHashSetTests.cs index a98348751b..df7f004884 100644 --- a/src/Umbraco.Tests/Collections/OrderedHashSetTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Collections/OrderedHashSetTests.cs @@ -1,9 +1,8 @@ using System; using NUnit.Framework; -using Umbraco.Core; using Umbraco.Core.Collections; -namespace Umbraco.Tests.Collections +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Collections { [TestFixture] public class OrderedHashSetTests @@ -12,7 +11,7 @@ namespace Umbraco.Tests.Collections public void Keeps_Last() { var list = new OrderedHashSet(keepOldest: false); - var items = new[] { new MyClass("test"), new MyClass("test"), new MyClass("test") }; + var items = new[] {new MyClass("test"), new MyClass("test"), new MyClass("test")}; foreach (var item in items) { list.Add(item); @@ -27,7 +26,7 @@ namespace Umbraco.Tests.Collections public void Keeps_First() { var list = new OrderedHashSet(keepOldest: true); - var items = new[] { new MyClass("test"), new MyClass("test"), new MyClass("test") }; + var items = new[] {new MyClass("test"), new MyClass("test"), new MyClass("test")}; foreach (var item in items) { list.Add(item); @@ -60,7 +59,7 @@ namespace Umbraco.Tests.Collections if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; - return Equals((MyClass)obj); + return Equals((MyClass) obj); } public override int GetHashCode() diff --git a/src/Umbraco.Tests/CoreThings/DelegateExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/DelegateExtensionsTests.cs similarity index 95% rename from src/Umbraco.Tests/CoreThings/DelegateExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/DelegateExtensionsTests.cs index 87b2f4c03b..a3e36c8ae6 100644 --- a/src/Umbraco.Tests/CoreThings/DelegateExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/DelegateExtensionsTests.cs @@ -3,7 +3,7 @@ using Lucene.Net.Index; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class DelegateExtensionsTests diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumExtensionsTests.cs similarity index 97% rename from src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumExtensionsTests.cs index faa15b0077..d5ea4d2677 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumExtensionsTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Web.Trees; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class EnumExtensionsTests diff --git a/src/Umbraco.Tests/CoreThings/EnumerableExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumerableExtensionsTests.cs similarity index 98% rename from src/Umbraco.Tests/CoreThings/EnumerableExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumerableExtensionsTests.cs index e734713c76..32e039f26a 100644 --- a/src/Umbraco.Tests/CoreThings/EnumerableExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/EnumerableExtensionsTests.cs @@ -1,10 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class EnumerableExtensionsTests diff --git a/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs similarity index 95% rename from src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs index 639d85b4ff..62e0955d78 100644 --- a/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/GuidUtilsTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { public class GuidUtilsTests { diff --git a/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/HexEncoderTests.cs similarity index 97% rename from src/Umbraco.Tests/CoreThings/HexEncoderTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/HexEncoderTests.cs index 588fff83e8..f22c3f2ac1 100644 --- a/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/HexEncoderTests.cs @@ -3,7 +3,7 @@ using System.Text; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { public class HexEncoderTests { diff --git a/src/Umbraco.Tests/Clr/ReflectionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionTests.cs similarity index 54% rename from src/Umbraco.Tests/Clr/ReflectionTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionTests.cs index f3704c8552..063a8c2621 100644 --- a/src/Umbraco.Tests/Clr/ReflectionTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.Clr +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class ReflectionTests @@ -25,37 +25,16 @@ namespace Umbraco.Tests.Clr Assert.Contains(typeof(object), types); } - [Test] - public void GetInterfacesIsOk() - { - // tests that GetInterfaces gets _all_ interfaces - // so the AllInterfaces extension method is useless - - var type = typeof(Class2); - var interfaces = type.GetInterfaces(); - Assert.AreEqual(2, interfaces.Length); - Assert.Contains(typeof(IInterface1), interfaces); - Assert.Contains(typeof(IInterface2), interfaces); - } - #region Test Objects - interface IInterface1 - { } - - interface IInterface2 : IInterface1 + private class Class1 { - void Method(); } - class Class1 : IInterface2 + private class Class2 : Class1 { - public void Method() { } } - class Class2 : Class1 - { } - #endregion } } diff --git a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionUtilitiesTests.cs similarity index 98% rename from src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionUtilitiesTests.cs index c885f364dc..0f48f2cea2 100644 --- a/src/Umbraco.Tests/Clr/ReflectionUtilitiesTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/ReflectionUtilitiesTests.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; +using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Core; -using System.Linq; -using Newtonsoft.Json; -namespace Umbraco.Tests.Clr +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class ReflectionUtilitiesTests @@ -104,8 +104,6 @@ namespace Umbraco.Tests.Clr [Test] public void EmitMethodEmitsStaticStatic() { - // static types cannot be used as type arguments - //var method = ReflectionUtilities.EmitMethod("Method"); var method = ReflectionUtilities.EmitMethod(typeof (StaticClass1), "Method"); method(); } @@ -205,10 +203,6 @@ namespace Umbraco.Tests.Clr (var getter3, var setter3) = ReflectionUtilities.EmitPropertyGetterAndSetter("Value3"); Assert.AreEqual(42, getter3(class1)); setter3(class1, 42); - - // this is not supported yet - //var getter4 = ReflectionUtilities.EmitPropertyGetter("Value1", returned: typeof(int)); - //Assert.AreEqual(42, getter1(class1)); } [Test] @@ -448,7 +442,7 @@ namespace Umbraco.Tests.Clr var propInt4 = type4.GetProperty("IntValue"); Assert.IsNotNull(propInt4); - // ... if explicitely getting a value type + // ... if explicitly getting a value type var getterInt4T = ReflectionUtilities.EmitPropertyGetter(propInt4); Assert.IsNotNull(getterInt4T); var valueInt4T = getterInt4T(object4); diff --git a/src/Umbraco.Tests/CoreThings/VersionExtensionTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/VersionExtensionTests.cs similarity index 86% rename from src/Umbraco.Tests/CoreThings/VersionExtensionTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/VersionExtensionTests.cs index 5799e28b87..a4ab15afe7 100644 --- a/src/Umbraco.Tests/CoreThings/VersionExtensionTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/VersionExtensionTests.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class VersionExtensionTests @@ -18,7 +18,7 @@ namespace Umbraco.Tests.CoreThings [TestCase(0, 0, 1, 1, "0.0.1.0")] [TestCase(0, 0, 0, 1, "0.0.0.0")] [TestCase(7, 3, 0, 0, "7.2.2147483647.2147483647")] - public void Subract_Revision(int major, int minor, int build, int rev, string outcome) + public void Subtract_Revision(int major, int minor, int build, int rev, string outcome) { var version = new Version(major, minor, build, rev); diff --git a/src/Umbraco.Tests/CoreThings/XmlExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/XmlExtensionsTests.cs similarity index 96% rename from src/Umbraco.Tests/CoreThings/XmlExtensionsTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Core/XmlExtensionsTests.cs index 62e385beff..ae3762ef57 100644 --- a/src/Umbraco.Tests/CoreThings/XmlExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/XmlExtensionsTests.cs @@ -3,7 +3,7 @@ using System.Xml.Linq; using NUnit.Framework; using Umbraco.Core; -namespace Umbraco.Tests.CoreThings +namespace Umbraco.Tests.UnitTests.Umbraco.Core { [TestFixture] public class XmlExtensionsTests diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 67d822fb03..24627b2722 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs b/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs index 63e481e9a5..f7b39590f2 100644 --- a/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs +++ b/src/Umbraco.Tests/Cache/DeepCloneAppCacheTests.cs @@ -12,7 +12,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; -using Umbraco.Tests.Collections; +using Umbraco.Tests.Common; using Umbraco.Tests.TestHelpers; using Umbraco.Web.Cache; @@ -41,12 +41,12 @@ namespace Umbraco.Tests.Cache [Test] public void Clones_List() { - var original = new DeepCloneableList(ListCloneBehavior.Always); - original.Add(new DeepCloneableListTests.TestClone()); - original.Add(new DeepCloneableListTests.TestClone()); - original.Add(new DeepCloneableListTests.TestClone()); + var original = new DeepCloneableList(ListCloneBehavior.Always); + original.Add(new TestClone()); + original.Add(new TestClone()); + original.Add(new TestClone()); - var val = _provider.GetCacheItem>("test", () => original); + var val = _provider.GetCacheItem>("test", () => original); Assert.AreEqual(original.Count, val.Count); foreach (var item in val) diff --git a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs index 4ec6dfc0d5..6819823dd5 100644 --- a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs +++ b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs @@ -333,6 +333,24 @@ namespace Umbraco.Tests.Composing Assert.AreSame(s1, s2); } + [Test] + public void CanRegisterMultipleSameTypeParametersWithCreateInstance() + { + var register = GetRegister(); + + register.Register(c => + { + const string param1 = "param1"; + const string param2 = "param2"; + + return c.CreateInstance(param1, param2); + }); + + var factory = register.CreateFactory(); + var instance = factory.GetInstance(); + Assert.AreNotEqual(instance.Thing, instance.AnotherThing); + } + public interface IThing { } public abstract class ThingBase : IThing { } @@ -353,5 +371,17 @@ namespace Umbraco.Tests.Composing public IEnumerable Things { get; } } + + public class Thing4 : ThingBase + { + public readonly string Thing; + public readonly string AnotherThing; + + public Thing4(string thing, string anotherThing) + { + Thing = thing; + AnotherThing = anotherThing; + } + } } } diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs new file mode 100644 index 0000000000..1b83c048d2 --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -0,0 +1,405 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class NestedContentPropertyComponentTests + { + [Test] + public void Invalid_Json() + { + var component = new NestedContentPropertyComponent(); + + Assert.DoesNotThrow(() => component.CreateNestedContentKeys("this is not json", true)); + } + + [Test] + public void No_Nesting() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[ + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} +]"; + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"": [{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ] + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + // Complex editor such as the grid + var complexEditorJsonEscaped = @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subJsonEscaped + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + complexEditorJsonEscaped + @" + } +]"; + + var expected = json + .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) + .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()) + .Replace("dccf550c-3a05-469e-95e1-a8f560f788c2", guids[2].ToString()) + .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + } + + + [Test] + public void No_Nesting_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"[ + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1 my key wont change"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""name"":""Item 2 was copied and has no key prop"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + + // Ensure that the original key is NOT changed/modified & still exists + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); + } + + [Test] + public void One_Level_Nesting_Escaped_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Nested Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + var json = @"[{ + ""name"": ""Item 1 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON for each item + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString())); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped_Generates_Keys_For_Missing_Items() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Nested Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + } + ]").ToString()); + + // Complex editor such as the grid + var complexEditorJsonEscaped = @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subJsonEscaped + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + + + var json = @"[{ + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""name"": ""Item 2 was copied and has no key"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + complexEditorJsonEscaped + @" + } +]"; + + var component = new NestedContentPropertyComponent(); + var result = component.CreateNestedContentKeys(json, true, guidFactory); + + // Ensure the new GUID is put in a key into the JSON for each item + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + } + } +} diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 0d98574504..ab9f85aa3c 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -99,8 +99,8 @@ namespace Umbraco.Tests.Services private void AssertJsonStartsWith(int id, string expected) { var json = GetJson(id).Replace('"', '\''); - var pos = json.IndexOf("'cultureData':", StringComparison.InvariantCultureIgnoreCase); - json = json.Substring(0, pos + "'cultureData':".Length); + var pos = json.IndexOf("'cd':", StringComparison.InvariantCultureIgnoreCase); + json = json.Substring(0, pos + "'cd':".Length); Assert.AreEqual(expected, json); } @@ -606,7 +606,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch content type to Nothing contentType.Variations = ContentVariation.Nothing; @@ -623,7 +623,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); // switch content back to Culture contentType.Variations = ContentVariation.Culture; @@ -640,7 +640,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); // switch property back to Culture contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; @@ -656,7 +656,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); } [Test] @@ -697,7 +697,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); // switch content type to Culture contentType.Variations = ContentVariation.Culture; @@ -713,7 +713,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); // switch property to Culture contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; @@ -728,7 +728,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); // switch content back to Nothing contentType.Variations = ContentVariation.Nothing; @@ -745,7 +745,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1'}],'value2':[{'v':'v2'}]},'cd':"); } [Test] @@ -783,7 +783,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch property type to Nothing contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; @@ -800,7 +800,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'','seg':'','val':'v1en'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'v':'v1en'}],'value2':[{'v':'v2'}]},'cd':"); // switch property back to Culture contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; @@ -816,7 +816,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'v':'v2'}]},'cd':"); // switch other property to Culture contentType.PropertyTypes.First(x => x.Alias == "value2").Variations = ContentVariation.Culture; @@ -834,7 +834,7 @@ namespace Umbraco.Tests.Services Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); + "{'pd':{'value1':[{'c':'en','v':'v1en'},{'c':'fr','v':'v1fr'}],'value2':[{'c':'en','v':'v2'}]},'cd':"); } [TestCase(ContentVariation.Culture, ContentVariation.Nothing)] @@ -1065,7 +1065,7 @@ namespace Umbraco.Tests.Services // both value11 and value21 are variant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composed.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composed); @@ -1073,7 +1073,7 @@ namespace Umbraco.Tests.Services // both value11 and value21 are invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); composed.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed); @@ -1081,7 +1081,7 @@ namespace Umbraco.Tests.Services // value11 is variant again, but value21 is still invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); composed.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed); @@ -1089,7 +1089,7 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composing); @@ -1097,7 +1097,7 @@ namespace Umbraco.Tests.Services // value11 is invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1105,7 +1105,7 @@ namespace Umbraco.Tests.Services // value11 is still invariant Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1113,7 +1113,7 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document.Id)); AssertJsonStartsWith(document.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); } [Test] @@ -1178,11 +1178,11 @@ namespace Umbraco.Tests.Services // both value11 and value21 are variant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composed1.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composed1); @@ -1190,11 +1190,11 @@ namespace Umbraco.Tests.Services // both value11 and value21 are invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composed1.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed1); @@ -1202,11 +1202,11 @@ namespace Umbraco.Tests.Services // value11 is variant again, but value21 is still invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'','seg':'','val':'v21en'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'v':'v21en'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composed1.PropertyTypes.First(x => x.Alias == "value21").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composed1); @@ -1214,11 +1214,11 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composing.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(composing); @@ -1226,11 +1226,11 @@ namespace Umbraco.Tests.Services // value11 is invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composing.Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1238,11 +1238,11 @@ namespace Umbraco.Tests.Services // value11 is still invariant Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11en'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11en'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); composing.PropertyTypes.First(x => x.Alias == "value11").Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(composing); @@ -1250,11 +1250,11 @@ namespace Umbraco.Tests.Services // we can make it variant again Console.WriteLine(GetJson(document1.Id)); AssertJsonStartsWith(document1.Id, - "{'properties':{'value11':[{'culture':'en','seg':'','val':'v11en'},{'culture':'fr','seg':'','val':'v11fr'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value21':[{'culture':'en','seg':'','val':'v21en'},{'culture':'fr','seg':'','val':'v21fr'}],'value22':[{'culture':'','seg':'','val':'v22'}]},'cultureData':"); + "{'pd':{'value11':[{'c':'en','v':'v11en'},{'c':'fr','v':'v11fr'}],'value12':[{'v':'v12'}],'value21':[{'c':'en','v':'v21en'},{'c':'fr','v':'v21fr'}],'value22':[{'v':'v22'}]},'cd':"); Console.WriteLine(GetJson(document2.Id)); AssertJsonStartsWith(document2.Id, - "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); + "{'pd':{'value11':[{'v':'v11'}],'value12':[{'v':'v12'}],'value31':[{'v':'v31'}],'value32':[{'v':'v32'}]},'cd':"); } private void CreateFrenchAndEnglishLangs() diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 80f6ab9c9e..bbc869fc65 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -313,6 +313,7 @@ namespace Umbraco.Tests.Testing Composition.RegisterUnique(); Composition.RegisterUnique(); + Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); Composition.RegisterUnique(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 5227182be6..a6d5da77db 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -117,17 +117,11 @@ - - - - - - @@ -157,6 +151,7 @@ + @@ -247,17 +242,14 @@ - - - @@ -307,7 +299,6 @@ - @@ -465,7 +456,6 @@ - @@ -474,10 +464,8 @@ - - diff --git a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs index a162b0cd48..f8f02560c4 100644 --- a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs @@ -91,7 +91,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - Factory.GetInstance() + Factory.GetInstance(), + Factory.GetInstance() ); return usersController; } diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 4c373b2bc8..ce355180f6 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -103,7 +103,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - Factory.GetInstance() + Factory.GetInstance(), + Factory.GetInstance() ); return usersController; @@ -176,7 +177,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - Factory.GetInstance() + Factory.GetInstance(), + Factory.GetInstance() ); return usersController; } @@ -219,7 +221,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - Factory.GetInstance() + Factory.GetInstance(), + Factory.GetInstance() ); return usersController; } @@ -297,7 +300,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - Factory.GetInstance() + Factory.GetInstance(), + Factory.GetInstance() ); return usersController; } @@ -487,7 +491,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - Factory.GetInstance()); + Factory.GetInstance(), + Factory.GetInstance()); var mockOwinContext = new Mock(); var mockUserManagerMarker = new Mock(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index b0466b4e49..30484d072b 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -176,9 +176,10 @@ namespace Umbraco.Web.Common.Extensions var globalSettings = configs.Global(); var connStrings = configs.ConnectionStrings(); var appSettingMainDomLock = globalSettings.MainDomLock; + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" || isLinux == true - ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator) + ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connStrings, dbProviderFactoryCreator, hostingEnvironment) : new MainDomSemaphoreLock(logger, hostingEnvironment); var mainDom = new MainDom(logger, mainDomLock); diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 67a47a0a15..7b157179c3 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -3,10 +3,16 @@ module.exports = { compile: { build: { - sourcemaps: false + sourcemaps: false, + embedtemplates: true }, dev: { - sourcemaps: true + sourcemaps: true, + embedtemplates: true + }, + test: { + sourcemaps: false, + embedtemplates: true } }, sources: { @@ -17,7 +23,7 @@ module.exports = { installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" }, nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"}, preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" }, - umbraco: { files: "./src/less/belle.less", watch: "./src/less/**/*.less", out: "umbraco.css" }, + umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.css" }, rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" } }, diff --git a/src/Umbraco.Web.UI.Client/gulp/modes.js b/src/Umbraco.Web.UI.Client/gulp/modes.js index dc2947f2cc..21609cdcf8 100644 --- a/src/Umbraco.Web.UI.Client/gulp/modes.js +++ b/src/Umbraco.Web.UI.Client/gulp/modes.js @@ -10,4 +10,14 @@ function setDevelopmentMode(cb) { return cb(); }; -module.exports = { setDevelopmentMode: setDevelopmentMode }; +function setTestMode(cb) { + + config.compile.current = config.compile.test; + + return cb(); +}; + +module.exports = { + setDevelopmentMode: setDevelopmentMode, + setTestMode: setTestMode + }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js index 1e8d074f7e..255fe17435 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js @@ -6,11 +6,24 @@ var karmaServer = require('karma').Server; * Build tests **************************/ - // Karma test +// Karma test function testUnit() { + return new karmaServer({ + configFile: __dirname + "/../../test/config/karma.conf.js" + }) + .start(); +}; + +// Run karma test server +function runUnitTestServer() { + return new karmaServer({ configFile: __dirname + "/../../test/config/karma.conf.js", + autoWatch: true, + port: 9999, + singleRun: false, + browsers: ['ChromeDebugging'], keepalive: true }) .start(); @@ -24,4 +37,4 @@ function testE2e() { .start(); }; -module.exports = { testUnit: testUnit, testE2e: testE2e }; +module.exports = { testUnit: testUnit, testE2e: testE2e, runUnitTestServer: runUnitTestServer }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index db24dae23e..744481391a 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -1,7 +1,7 @@ 'use strict'; const config = require('../config'); -const {watch, parallel, dest, src} = require('gulp'); +const {watch, series, parallel, dest, src} = require('gulp'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -9,9 +9,7 @@ var MergeStream = require('merge-stream'); var processJs = require('../util/processJs'); var processLess = require('../util/processLess'); -//const { less } = require('./less'); -//const { views } = require('./views'); - +var {js} = require('./js'); function watchTask(cb) { @@ -30,24 +28,27 @@ function watchTask(cb) { watch(group.watch, { ignoreInitial: true, interval: watchInterval }, function Less_Group_Compile() { return processLess(group.files, group.out); }); } }); - + //Setup a watcher for all groups of view files var viewWatcher; _.forEach(config.sources.views, function (group) { if(group.watch !== false) { - viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }); - viewWatcher.on('change', function(path, stats) { - - var task = src(group.files); + viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }, + parallel( + function MoveViewsAndRegenerateJS() { + var task = src(group.files); - _.forEach(config.roots, function(root){ - console.log("copying " + group.files + " to " + root + config.targets.views + group.folder); - task = task.pipe( dest(root + config.targets.views + group.folder) ); - }) - }); + _.forEach(config.roots, function(root){ + console.log("copying " + group.files + " to " + root + config.targets.views + group.folder); + task = task.pipe( dest(root + config.targets.views + group.folder) ); + }); + }, + js + ) + ); } }); - + return cb(); }; diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js index 9ff9a93c09..6c6f1276e7 100644 --- a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js +++ b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js @@ -28,7 +28,9 @@ module.exports = function (files, out) { .pipe(sort()); //in production, embed the templates - task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })) + if(config.compile.current.embedtemplates === true) { + task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })); + } task = task.pipe(concat(out)).pipe(wrap('(function(){\n%= body %\n})();')) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 705c54bf04..542d45c479 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -13,11 +13,11 @@ const { src, dest, series, parallel, lastRun } = require('gulp'); const config = require('./gulp/config'); -const { setDevelopmentMode } = require('./gulp/modes'); +const { setDevelopmentMode, setTestMode } = require('./gulp/modes'); const { dependencies } = require('./gulp/tasks/dependencies'); const { js } = require('./gulp/tasks/js'); const { less } = require('./gulp/tasks/less'); -const { testE2e, testUnit } = require('./gulp/tasks/test'); +const { testE2e, testUnit, runUnitTestServer } = require('./gulp/tasks/test'); const { views } = require('./gulp/tasks/views'); const { watchTask } = require('./gulp/tasks/watchTask'); @@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit); exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask); exports.watch = series(watchTask); // -exports.runTests = series(js, testUnit); -exports.testUnit = series(testUnit); -exports.testE2e = series(testE2e); +exports.runTests = series(setTestMode, parallel(js, testUnit)); +exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask); +exports.testE2e = series(setTestMode, parallel(testE2e)); diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js new file mode 100644 index 0000000000..90eae85800 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js @@ -0,0 +1,261 @@ +tinymce.addI18n('en_US',{ +"Redo": "Redo", +"Undo": "Undo", +"Cut": "Cut", +"Copy": "Copy", +"Paste": "Paste", +"Select all": "Select all", +"New document": "New document", +"Ok": "Ok", +"Cancel": "Cancel", +"Visual aids": "Visual aids", +"Bold": "Bold", +"Italic": "Italic", +"Underline": "Underline", +"Strikethrough": "Strikethrough", +"Superscript": "Superscript", +"Subscript": "Subscript", +"Clear formatting": "Clear formatting", +"Align left": "Align left", +"Align center": "Align center", +"Align right": "Align right", +"Justify": "Justify", +"Bullet list": "Bullet list", +"Numbered list": "Numbered list", +"Decrease indent": "Decrease indent", +"Increase indent": "Increase indent", +"Close": "Close", +"Formats": "Formats", +"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.", +"Headers": "Headers", +"Header 1": "Header 1", +"Header 2": "Header 2", +"Header 3": "Header 3", +"Header 4": "Header 4", +"Header 5": "Header 5", +"Header 6": "Header 6", +"Headings": "Headings", +"Heading 1": "Heading 1", +"Heading 2": "Heading 2", +"Heading 3": "Heading 3", +"Heading 4": "Heading 4", +"Heading 5": "Heading 5", +"Heading 6": "Heading 6", +"Preformatted": "Preformatted", +"Div": "Div", +"Pre": "Pre", +"Code": "Code", +"Paragraph": "Paragraph", +"Blockquote": "Blockquote", +"Inline": "Inline", +"Blocks": "Blocks", +"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.", +"Font Family": "Font Family", +"Font Sizes": "Font Sizes", +"Class": "Class", +"Browse for an image": "Browse for an image", +"OR": "OR", +"Drop an image here": "Drop an image here", +"Upload": "Upload", +"Block": "Blocks", +"Align": "Align", +"Default": "Default", +"Circle": "Circle", +"Disc": "Disc", +"Square": "Square", +"Lower Alpha": "Lower Alpha", +"Lower Greek": "Lower Greek", +"Lower Roman": "Lower Roman", +"Upper Alpha": "Upper Alpha", +"Upper Roman": "Upper Roman", +"Anchor": "Anchor", +"Name": "Name", +"Id": "ID", +"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "ID should start with a letter, followed only by letters, numbers, dashes, dots, colons, or underscores.", +"You have unsaved changes are you sure you want to navigate away?": "You have unsaved changes are you sure you want to navigate away?", +"Restore last draft": "Restore last draft", +"Special character": "Special character", +"Source code": "Source code", +"Insert\/Edit code sample": "Insert\/Edit code sample", +"Language": "Language", +"Code sample": "Code sample", +"Color": "color", +"R": "R", +"G": "G", +"B": "B", +"Left to right": "Left to right", +"Right to left": "Right to left", +"Emoticons": "Emoticons", +"Document properties": "Document properties", +"Title": "Title", +"Keywords": "Keywords", +"Description": "Description", +"Robots": "Robots", +"Author": "Author", +"Encoding": "Encoding", +"Fullscreen": "Fullscreen", +"Action": "Action", +"Shortcut": "Shortcut", +"Help": "Help", +"Address": "Address", +"Focus to menubar": "Focus to menubar", +"Focus to toolbar": "Focus to toolbar", +"Focus to element path": "Focus to element path", +"Focus to contextual toolbar": "Focus to contextual toolbar", +"Insert link (if link plugin activated)": "Insert link (if link plugin activated)", +"Save (if save plugin activated)": "Save (if save plugin activated)", +"Find (if searchreplace plugin activated)": "Find (if searchreplace plugin activated)", +"Plugins installed ({0}):": "Plugins installed ({0}):", +"Premium plugins:": "Premium plugins:", +"Learn more...": "Learn more...", +"You are using {0}": "You are using {0}", +"Plugins": "Plugins", +"Handy Shortcuts": "Handy Shortcuts", +"Horizontal line": "Horizontal line", +"Insert\/edit image": "Insert\/edit image", +"Image description": "Image description", +"Source": "Source", +"Dimensions": "Dimensions", +"Constrain proportions": "Constrain proportions", +"General": "General", +"Advanced": "Advanced", +"Style": "Style", +"Vertical space": "Vertical space", +"Horizontal space": "Horizontal space", +"Border": "Border", +"Insert image": "Insert image", +"Image": "Image", +"Image list": "Image list", +"Rotate counterclockwise": "Rotate counterclockwise", +"Rotate clockwise": "Rotate clockwise", +"Flip vertically": "Flip vertically", +"Flip horizontally": "Flip horizontally", +"Edit image": "Edit image", +"Image options": "Image options", +"Zoom in": "Zoom in", +"Zoom out": "Zoom out", +"Crop": "Crop", +"Resize": "Resize", +"Orientation": "Orientation", +"Brightness": "Brightness", +"Sharpen": "Sharpen", +"Contrast": "Contrast", +"Color levels": "color levels", +"Gamma": "Gamma", +"Invert": "Invert", +"Apply": "Apply", +"Back": "Back", +"Insert date\/time": "Insert date\/time", +"Date\/time": "Date\/time", +"Insert link": "Insert link", +"Insert\/edit link": "Insert\/edit link", +"Text to display": "Text to display", +"Url": "Url", +"Target": "Target", +"None": "None", +"New window": "New window", +"Remove link": "Remove link", +"Anchors": "Anchors", +"Link": "Link", +"Paste or type a link": "Paste or type a link", +"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?", +"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?", +"Link list": "Link list", +"Insert video": "Insert video", +"Insert\/edit video": "Insert\/edit video", +"Insert\/edit media": "Insert\/edit media", +"Alternative source": "Alternative source", +"Poster": "Poster", +"Paste your embed code below:": "Paste your embed code below:", +"Embed": "Embed", +"Media": "Media", +"Nonbreaking space": "Nonbreaking space", +"Page break": "Page break", +"Paste as text": "Paste as text", +"Preview": "Preview", +"Print": "Print", +"Save": "Save", +"Find": "Find", +"Replace with": "Replace with", +"Replace": "Replace", +"Replace all": "Replace all", +"Prev": "Prev", +"Next": "Next", +"Find and replace": "Find and replace", +"Could not find the specified string.": "Could not find the specified string.", +"Match case": "Match case", +"Whole words": "Whole words", +"Spellcheck": "Spellcheck", +"Ignore": "Ignore", +"Ignore all": "Ignore all", +"Finish": "Finish", +"Add to Dictionary": "Add to Dictionary", +"Insert table": "Insert table", +"Table properties": "Table properties", +"Delete table": "Delete table", +"Cell": "Cell", +"Row": "Row", +"Column": "Column", +"Cell properties": "Cell properties", +"Merge cells": "Merge cells", +"Split cell": "Split cell", +"Insert row before": "Insert row before", +"Insert row after": "Insert row after", +"Delete row": "Delete row", +"Row properties": "Row properties", +"Cut row": "Cut row", +"Copy row": "Copy row", +"Paste row before": "Paste row before", +"Paste row after": "Paste row after", +"Insert column before": "Insert column before", +"Insert column after": "Insert column after", +"Delete column": "Delete column", +"Cols": "Cols", +"Rows": "Rows", +"Width": "Width", +"Height": "Height", +"Cell spacing": "Cell spacing", +"Cell padding": "Cell padding", +"Caption": "Caption", +"Left": "Left", +"Center": "Center", +"Right": "Right", +"Cell type": "Cell type", +"Scope": "Scope", +"Alignment": "Alignment", +"H Align": "H Align", +"V Align": "V Align", +"Top": "Top", +"Middle": "Middle", +"Bottom": "Bottom", +"Header cell": "Header cell", +"Row group": "Row group", +"Column group": "Column group", +"Row type": "Row type", +"Header": "Header", +"Body": "Body", +"Footer": "Footer", +"Border color": "Border color", +"Insert template": "Insert template", +"Templates": "Templates", +"Template": "Template", +"Text color": "Text color", +"Background color": "Background color", +"Custom...": "Custom...", +"Custom color": "Custom color", +"No color": "No color", +"Table of Contents": "Table of Contents", +"Show blocks": "Show blocks", +"Show invisible characters": "Show invisible characters", +"Words: {0}": "Words: {0}", +"{0} words": "{0} words", +"File": "File", +"Edit": "Edit", +"Insert": "Insert", +"View": "View", +"Format": "Format", +"Table": "Table", +"Tools": "Tools", +"Powered by {0}": "Powered by {0}", +"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help" +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 8ab5980107..c298f063a7 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -42,7 +42,7 @@ "npm": "6.13.6", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", - "tinymce": "4.9.9", + "tinymce": "4.9.10", "typeahead.js": "0.11.1", "underscore": "1.9.1" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 9117ab548c..cb5a9c712a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -63,10 +63,14 @@ vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? "general_email" : "general_username", - vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint"] + vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint", + vm.usernameIsEmail ? "placeholders_emptyEmail" : "placeholders_emptyUsername", + "placeholders_emptyPassword"] ).then(function (data) { vm.labels.usernameLabel = data[0]; vm.labels.usernamePlaceholder = data[1]; + vm.labels.usernameError = data[2]; + vm.labels.passwordError = data[3]; }); vm.twoFactor = {}; @@ -193,70 +197,70 @@ } function loginSubmit() { - - // make sure that we are returning to the login view. - vm.view = "login"; - - // TODO: Do validation properly like in the invite password update + + if (formHelper.submitForm({ scope: $scope })) { + //if the login and password are not empty we need to automatically + // validate them - this is because if there are validation errors on the server + // then the user has to change both username & password to resubmit which isn't ideal, + // so if they're not empty, we'll just make sure to set them to valid. + if (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + + if (vm.loginForm.$invalid) { + SetTitle(); + return; + } + + // make sure that we are returning to the login view. + vm.view = "login"; - //if the login and password are not empty we need to automatically - // validate them - this is because if there are validation errors on the server - // then the user has to change both username & password to resubmit which isn't ideal, - // so if they're not empty, we'll just make sure to set them to valid. - if (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } + vm.loginStates.submitButton = "busy"; - if (vm.loginForm.$invalid) { - return; - } + userService.authenticate(vm.login, vm.password) + .then(function(data) { + vm.loginStates.submitButton = "success"; + userService._retryRequestQueue(true); + if (vm.onLogin) { + vm.onLogin(); + } + }, + function(reason) { - vm.loginStates.submitButton = "busy"; + //is Two Factor required? + if (reason.status === 402) { + vm.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView); + } else { + vm.loginStates.submitButton = "error"; + vm.errorMsg = reason.errorMsg; - userService.authenticate(vm.login, vm.password) - .then(function (data) { - vm.loginStates.submitButton = "success"; - userService._retryRequestQueue(true); - if(vm.onLogin) { - vm.onLogin(); + //set the form inputs to invalid + vm.loginForm.username.$setValidity("auth", false); + vm.loginForm.password.$setValidity("auth", false); + } + + userService._retryRequestQueue(); + + }); + + //setup a watch for both of the model values changing, if they change + // while the form is invalid, then revalidate them so that the form can + // be submitted again. + vm.loginForm.username.$viewChangeListeners.push(function() { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); } - }, - function (reason) { - - //is Two Factor required? - if (reason.status === 402) { - vm.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView); - } - else { - vm.loginStates.submitButton = "error"; - vm.errorMsg = reason.errorMsg; - - //set the form inputs to invalid - vm.loginForm.username.$setValidity("auth", false); - vm.loginForm.password.$setValidity("auth", false); - } - - userService._retryRequestQueue(); - }); - - //setup a watch for both of the model values changing, if they change - // while the form is invalid, then revalidate them so that the form can - // be submitted again. - vm.loginForm.username.$viewChangeListeners.push(function () { - if (vm.loginForm.$invalid) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } - }); - vm.loginForm.password.$viewChangeListeners.push(function () { - if (vm.loginForm.$invalid) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } - }); + vm.loginForm.password.$viewChangeListeners.push(function() { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + } } function requestPasswordResetSubmit(email) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 6f98dbca6e..1a9c3c771a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -5,14 +5,14 @@ @scope @description -Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. -In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. -You can easily add you own tours to the Help-drawer or show and start tours from +Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. +In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. +You can easily add you own tours to the Help-drawer or show and start tours from anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install The Starter Kit in Umbraco 7.8

Extending the help drawer with custom tours

-The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. -Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be +The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. +Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be picked up by Umbraco and shown in the Help-drawer.

The tour object

@@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list "groupOrder": 200 // Control the order of tour groups "allowDisable": // Adds a "Don't" show this tour again"-button to the intro step "culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users - "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. + "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. "steps": [] // tour steps - see next example } @@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list "backdropOpacity": 0.4 // the backdrop opacity "view": "" // add a custom view "customProperties" : {} // add any custom properties needed for the custom view + "skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step }

Adding tours to other parts of the Umbraco backoffice

-It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, +It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.

Using the tour service

@@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um "element": "[data-element='my-tour-button']", "title": "Click the button", "content": "Click the button", - "event": "click" + "event": "click", + "skipStepIfVisible": "[data-element='my-other-tour-button']" } ] }; @@ -257,9 +259,27 @@ In the following example you see how to run some custom logic before a step goes // make sure we don't go too far if (scope.model.currentStepIndex !== scope.model.steps.length) { + + var upcomingStep = scope.model.steps[scope.model.currentStepIndex]; + + // If the currentStep JSON object has 'skipStepIfVisible' + // It's a DOM selector - if we find it then we ship over this step + if (upcomingStep.skipStepIfVisible) { + let tryFindDomEl = document.querySelector(upcomingStep.skipStepIfVisible); + if (tryFindDomEl) { + // check if element is visible: + if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) { + // if it was visible then we skip the step. + nextStep(); + return; + } + } + } + startStep(); // tour completed - final step } else { + // tour completed - final step scope.loadingStep = true; waitForPendingRerequests().then(function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index 87053c083c..58f799e5af 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -206,7 +206,7 @@ Use this directive to construct a header inside the main editor window. (function () { 'use strict'; - function EditorHeaderDirective(editorService, localizationService, editorState) { + function EditorHeaderDirective(editorService, localizationService, editorState, $rootScope) { function link(scope, $injector) { @@ -224,27 +224,9 @@ Use this directive to construct a header inside the main editor window. if (editorState.current) { //to do make work for user create/edit // to do make it work for user group create/ edit - // to do make it work for language edit/create - // to do make it work for log viewer - scope.isNew = editorState.current.id === 0 || - editorState.current.id === "0" || - editorState.current.id === -1 || - editorState.current.id === 0 || - editorState.current.id === "-1"; - - var localizeVars = [ - scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit", - "visuallyHiddenTexts_name", - scope.isNew ? "general_new" : "general_edit" - ]; - - if (scope.editorfor) { - localizeVars.push(scope.editorfor); - } - localizationService.localizeMany(localizeVars).then(function(data) { - setAccessibilityForEditor(data); - scope.loading = false; - }); + // to make it work for language edit/create + setAccessibilityForEditorState(); + scope.loading = false; } else { scope.loading = false; } @@ -283,59 +265,91 @@ Use this directive to construct a header inside the main editor window. editorService.iconPicker(iconPicker); }; - function setAccessibilityForEditor(data) { - - if (editorState.current) { - if (scope.nameLocked) { - scope.accessibility.a11yName = scope.name; - SetPageTitle(scope.name); - } else { - - scope.accessibility.a11yMessage = data[0]; - scope.accessibility.a11yName = data[1]; - var title = data[2] + ":"; - if (!scope.isNew) { - scope.accessibility.a11yMessage += " " + scope.name; - title += " " + scope.name; - } else { - var name = ""; - if (editorState.current.contentTypeName) { - name = editorState.current.contentTypeName; - } else if (scope.editorfor) { - name = data[3]; - } - if (name !== "") { - scope.accessibility.a11yMessage += " " + name; - scope.accessibility.a11yName = name + " " + scope.accessibility.a11yName; - title += " " + name; - } - } - if (title !== data[2] + ":") { - SetPageTitle(title); - } - - } - scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage); - scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName); + function setAccessibilityForEditorState() { + var isNew = editorState.current.id === 0 || + editorState.current.id === "0" || + editorState.current.id === -1 || + editorState.current.id === 0 || + editorState.current.id === "-1"; + + var contentTypeName = ""; + if (editorState.current.contentTypeName) { + contentTypeName = editorState.current.contentTypeName; } - + + var setTitle = false; + if (scope.setpagetitle !== undefined) { + setTitle = scope.setpagetitle; + } + setAccessibilityHeaderDirective(isNew, scope.editorfor, scope.nameLocked, scope.name, contentTypeName, setTitle); } + function setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, entityName, contentTypeName, setTitle) { + + var localizeVars = [ + isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit", + "visuallyHiddenTexts_name", + isNew ? "general_new" : "general_edit" + ]; + + if (editorFor) { + localizeVars.push(editorFor); + } + localizationService.localizeMany(localizeVars).then(function(data) { + if (nameLocked) { + scope.accessibility.a11yName = entityName; + if (setTitle) { + SetPageTitle(entityName); + } + } else { + + scope.accessibility.a11yMessage = data[0]; + scope.accessibility.a11yName = data[1]; + var title = data[2] + ":"; + if (!isNew) { + scope.accessibility.a11yMessage += " " + entityName; + title += " " + entityName; + } else { + var name = ""; + if (contentTypeName) { + name = editorState.current.contentTypeName; + } else if (editorFor) { + name = data[3]; + } + if (name !== "") { + scope.accessibility.a11yMessage += " " + name; + scope.accessibility.a11yName = name + " " + scope.accessibility.a11yName; + title += " " + name; + } + } + if (setTitle && title !== data[2] + ":") { + SetPageTitle(title); + } + + } + scope.accessibility.a11yMessageVisible = !isEmptyOrSpaces(scope.accessibility.a11yMessage); + scope.accessibility.a11yNameVisible = !isEmptyOrSpaces(scope.accessibility.a11yName); + + }); + } + + + function isEmptyOrSpaces(str) { return str === null || str===undefined || str.trim ===''; } function SetPageTitle(title) { - var setTitle = false; - if (scope.setpagetitle !== undefined) { - setTitle = scope.setpagetitle; - } - if (setTitle) { scope.$emit("$changeTitle", title); - } } + + $rootScope.$on('$setAccessibleHeader', function (event, isNew, editorFor, nameLocked, name, contentTypeName, setTitle) { + setAccessibilityHeaderDirective(isNew, editorFor, nameLocked, name, contentTypeName, setTitle); + }); } + + var directive = { transclude: true, restrict: 'E', diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js index 9a9d6d4a76..389aec2044 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js @@ -32,6 +32,7 @@ @param {boolean} required Set the checkbox to be required. @param {callback} onChange Callback when the value of the checkbox change by interaction. @param {string} cssClass Set a css class modifier +@param {boolean} disableDirtyCheck Disable checking if the model is dirty **/ @@ -84,7 +85,8 @@ required: "<", onChange: "&?", cssClass: "@?", - iconClass: "@?" + iconClass: "@?", + disableDirtyCheck: "=?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 9c33b35e82..ad62bcd3db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -16,10 +16,15 @@ angular.module("umbraco.directives") replace: true, templateUrl: 'views/components/property/umb-property.html', link: function (scope) { - userService.getCurrentUser().then(function (u) { - var isAdmin = u.userGroups.indexOf('admin') !== -1; - scope.propertyAlias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.property.alias : null; - }); + + scope.controlLabelTitle = null; + if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if(u.allowedSections.indexOf("settings") !== -1 ? true : false) { + scope.controlLabelTitle = scope.property.alias; + } + }); + } }, //Define a controller for this directive to expose APIs to other directives controller: function ($scope) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js index 19a33a8351..8d223e427e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblightbox.directive.js @@ -12,9 +12,9 @@
prepareEntryForStorage(data)); + var copiedDatas = datas.map(data => prepareEntryForStorage(data, firstLevelClearupMethod)); // remove previous copies of this entry: storage.entries = storage.entries.filter( diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 9e0285d58d..38aee3fc4a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -39,18 +39,22 @@ function fileManager($rootScope) { args.culture = null; } + if (!args.segment) { + args.segment = null; + } + var metaData = []; if (Utilities.isArray(args.metaData)) { metaData = args.metaData; } - //this will clear the files for the current property/culture and then add the new ones for the current property + //this will clear the files for the current property/culture/segment and then add the new ones for the current property fileCollection = _.reject(fileCollection, function (item) { - return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture); + return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture) && (!args.segment || args.segment === item.segment); }); for (var i = 0; i < args.files.length; i++) { //save the file object to the files collection - fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, metaData: metaData }); + fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, segment: args.segment, metaData: metaData }); } }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index edf698c8a7..4cbc5e567a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -252,12 +252,13 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe for (var f in args.files) { //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key // so we know which property it belongs to on the server side - var fileKey = "file_" + args.files[f].alias + "_" + (args.files[f].culture ? args.files[f].culture : ""); + var file = args.files[f]; + var fileKey = "file_" + file.alias + "_" + (file.culture ? file.culture : "") + "_" + (file.segment ? file.segment : ""); - if (Utilities.isArray(args.files[f].metaData) && args.files[f].metaData.length > 0) { - fileKey += ("_" + args.files[f].metaData.join("_")); + if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { + fileKey += ("_" + file.metaData.join("_")); } - formData.append(fileKey, args.files[f].file); + formData.append(fileKey, file.file); } }).then(function (response) { //success callback diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 174f9f41d7..f0d7c6f1e1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -227,6 +227,7 @@ @import "dashboards/umbraco-forms.less"; @import "dashboards/examine-management.less"; @import "dashboards/healthcheck.less"; +@import "dashboards/content-templates.less"; @import "dashboards/nucache.less"; @import "typeahead.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index a52f81b92a..8a24d948ac 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -18,6 +18,8 @@ label.umb-form-check--checkbox{ } .umb-form-check__info { margin-left:20px; + position: relative; + top: 3px; } @@ -46,6 +48,9 @@ label.umb-form-check--checkbox{ &:checked ~ .umb-form-check__state .umb-form-check__check { border-color: @ui-option-type; } + &[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check { + background-color: @ui-option-type; + } &:checked:hover ~ .umb-form-check__state .umb-form-check__check { &::before { background: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less index 96636a3096..6238900bf6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less @@ -32,14 +32,18 @@ .umb-lightbox__close { position: absolute; top: 20px; - right: 60px; + right: 20px; + height: 40px; + width: 40px; } .umb-lightbox__close i { font-size: 20px; - cursor: pointer; - height: 40px; - width: 40px; + height: inherit; + width: inherit; + position: absolute; + top: 0; + left: 0; } .umb-lightbox__images { @@ -75,12 +79,20 @@ right: 20px; top: 50%; transform: translate(0, -50%); + + .umb-lightbox__control-icon { + margin-right: -4px; + } } .umb-lightbox__control.-prev { left: 20px; top: 50%; transform: translate(0, -50%); + + .umb-lightbox__control-icon { + margin-left: -4px; + } } .umb-lightbox__control-icon { diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/content-templates.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/content-templates.less new file mode 100644 index 0000000000..9966fc97e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/content-templates.less @@ -0,0 +1,22 @@ +.content-templates-dashboard{ + p{ + line-height: 1.6em; + margin-bottom: 30px; + + &:last-child{ + margin-bottom: 0; + } + } + + ul{ + margin-bottom: 15px; + } + + li{ + margin-bottom: 5px; + + &:last-child{ + margin-bottom: 0; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index e36acdc273..818b1d84d1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -123,6 +123,7 @@ position: relative; text-align: right; user-select: none; + margin-left: auto; a { opacity: .5; @@ -134,8 +135,8 @@ .password-text { background-repeat: no-repeat; background-size: 18px; - background-position: left center; - padding-left: 26px; + background-position: 0px 1px; + padding-left: 24px; &.show { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 764b73c593..b5870b8dce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -863,11 +863,11 @@ .bootstrap-datetimepicker-widget .picker-switch .btn{ background: none; border: none;} .umb-datepicker .input-append .add-on{cursor: pointer;} .umb-datepicker .input-append .on-top { + border: 0 none; position: absolute; margin-left: -31px; margin-top: 1px; display: inline-block; - height: 22px; padding: 5px 6px 3px 6px; font-size: @baseFontSize; font-weight: normal; diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 81eadf150f..297d93f4bc 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -56,12 +56,14 @@ function MainController($scope, $location, appState, treeService, notificationsS appState.setSearchState("show", false); }; - $scope.showLoginScreen = function(isTimedOut) { + $scope.showLoginScreen = function (isTimedOut) { + $scope.login.pageTitle = $scope.$root.locationTitle; $scope.login.isTimedOut = isTimedOut; $scope.login.show = true; }; - $scope.hideLoginScreen = function() { + $scope.hideLoginScreen = function () { + $scope.$root.locationTitle = $scope.login.pageTitle; $scope.login.show = false; }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html index aa99248dfc..06162d2961 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html @@ -18,15 +18,14 @@
- +
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html index dca3849491..a3530f77b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html @@ -3,7 +3,7 @@
-
{{ name }}
+
{{ name }}
{{ description }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html index ce423225f6..66695ace91 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/intro.html @@ -12,21 +12,32 @@ - -

What are Content Templates?

-

Content Templates are pre-defined content that can be selected when creating a new content node.

+ +

+ What are Content Templates? +

+

+ Content Templates are pre-defined content that can be selected when creating a new content node. +

-

How do I create a Content Template?

-

There are two ways to create a Content Template:

-
    -
  • Right-click a content node and select "Create Content Template" to create a new Content Template.
  • -
  • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
  • -
-

Once given a name, editors can start using the Content Template as a foundation for their new page.

+

+ How do I create a Content Template? +

+ +

There are two ways to create a Content Template:

+
    +
  • Right-click a content node and select "Create Content Template" to create a new Content Template.
  • +
  • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
  • +
+

Once given a name, editors can start using the Content Template as a foundation for their new page.

+
-

How do I manage Content Templates

-

You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Just expand the Document Type which the - Content Template is based on and click it to edit or delete it.

+

+ How do I manage Content Templates? +

+

+ You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Just expand the Document Type which the Content Template is based on and click it to edit or delete it. +

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index e697dc56a5..b35663c3df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -20,10 +20,11 @@ ng-required="model.validation.mandatory" val-server="value" class="datepickerinput"> - - - - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js index a8979c949b..afbb4feb20 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js @@ -15,7 +15,14 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo //ensure this is a bool, old data could store zeros/ones or string versions $scope.model.config.multiple = Object.toBoolean($scope.model.config.multiple); - + + //ensure when form is saved that we don't store [] or [null] as string values in the database when no items are selected + $scope.$on("formSubmitting", function () { + if ($scope.model.value.length === 0 || $scope.model.value[0] === null) { + $scope.model.value = null; + } + }); + function convertArrayToDictionaryArray(model){ //now we need to format the items in the dictionary because we always want to have an array var newItems = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index 15f8dbf11c..71e334aee2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -34,6 +34,7 @@ fileManager.setFiles({ propertyAlias: $scope.model.alias, culture: $scope.model.culture, + segment: $scope.model.segment, files: [] }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index 2bc609714a..522278e99e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -1,5 +1,6 @@ 
Add - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 7cad5a5f05..066cbd6c17 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -1,6 +1,58 @@ (function () { 'use strict'; + /** + * When performing a copy, we do copy the ElementType Data Model, but each inner Nested Content property is still stored as the Nested Content Model, aka. each property is just storing its value. To handle this we need to ensure we handle both scenarios. + */ + + + angular.module('umbraco').run(['clipboardService', function (clipboardService) { + + function clearNestedContentPropertiesForStorage(prop, propClearingMethod) { + + // if prop.editor is "Umbraco.NestedContent" + if ((typeof prop === 'object' && prop.editor === "Umbraco.NestedContent")) { + + var value = prop.value; + for (var i = 0; i < value.length; i++) { + var obj = value[i]; + + // remove the key + delete obj.key; + + // Loop through all inner properties: + for (var k in obj) { + propClearingMethod(obj[k]); + } + } + } + } + + clipboardService.registrerClearPropertyResolver(clearNestedContentPropertiesForStorage) + + + function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { + + // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property inside a NestedContent property. + if ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { + + for (var i = 0; i < prop.length; i++) { + var obj = prop[i]; + + // remove the key + delete obj.key; + + // Loop through all inner properties: + for (var k in obj) { + propClearingMethod(obj[k]); + } + } + } + } + + clipboardService.registrerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) + }]); + angular .module('umbraco') .component('nestedContentPropertyEditor', { @@ -13,7 +65,7 @@ } }); - function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService, $routeParams, editorState) { + function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { var vm = this; var model = $scope.$parent.$parent.model; @@ -76,7 +128,7 @@ } localizationService.localize("clipboard_labelForArrayOfItemsFrom", [model.label, nodeName]).then(function (data) { - clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id); + clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); }); } @@ -385,6 +437,11 @@ }); } + function clearNodeForCopy(clonedData) { + delete clonedData.key; + delete clonedData.$$hashKey; + } + vm.showCopy = clipboardService.isSupported(); vm.showPaste = false; @@ -392,7 +449,7 @@ syncCurrentNode(); - clipboardService.copy("elementType", node.contentTypeAlias, node); + clipboardService.copy("elementType", node.contentTypeAlias, node, null, null, null, clearNodeForCopy); $event.stopPropagation(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index d255c4a5d6..87f6ffeac9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -1,6 +1,6 @@
- + {{mandatoryMessage}} diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html index 35e7aa5176..4b51c44c18 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html @@ -6,6 +6,7 @@ ul.nav > li.module { - background-color: #daf0c9; + background-color: #3544b1; } -.breadcrumb { - background-color: #f5fbf1; +.form-search > ul.nav > li.module a { + color: white; } -a { - color: #f36f21; +.form-search > ul.nav > li.section { + background-color: #ccc; } + +.breadcrumb { + background-color: #f7f7f7; +} + a:hover { text-decoration: none; color: rgba(0,0,0,.8); @@ -87,9 +93,12 @@ a:hover { color: #000; } +.form-search > ul.nav > li.module > a:hover{ + color: #fff; +} + .header img { width: 50px; - margin-top: 5px; } .content .methods code { diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 46dc20034b..95fdaeec67 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -116,6 +116,16 @@ "Replacement": "" } ] + }, + "Global": { + "Smtp": { + "From": "noreply@example.com", + "Host": "127.0.0.1", + "Port": 25, + "UserName": "username", + "Password": "password", + "DeliveryMethod": "network" + } } } } diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 7cead86114..a876b1a6b7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -247,7 +247,7 @@ Dette dokument er udgivet, men dets URL kan ikke dirigeres Udgiv Udgivet - Udgivet (Afventende ændringer) + Udgivet (Ventede ændringer) Udgivelsesstatus Udgiv med undersider for at udgive %0% og alle sider under og dermed gøre deres indhold offentligt tilgængelige.]]> Udgiv med undersider for at udgive de valgte sprog og de samme sprog for sider under og dermed gøre deres indhold offentligt tilgængelige.]]> @@ -426,6 +426,7 @@ Internt link: Ved lokalt link, indsæt da en "#" foran linket Åben i nyt vindue? + Makroindstillinger Denne makro har ingen egenskaber du kan redigere Indsæt tekst Rediger rettigheder for @@ -663,6 +664,7 @@ Ikon Id Importer + Inkludér undermapper i søgning Søg kun i denne mappe Info Indre margen @@ -1749,6 +1751,12 @@ Mange hilsner fra Umbraco robotten Åben backoffice søgning Åben/Luk backoffice hjælp Åben/Luk dine profil indstillinger + Tilføj domæne på %0% + Opret ny node under %0% + Opsæt offentlig adgang på %0% + Opsæt rettigheder på %0% + Juster soterings rækkefølgen for %0% + Opret indholds skabelon baseret på %0% Aktivt sprog Skift sprog til Opret ny mappe diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d7ccb13193..e96ef754c4 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -141,6 +141,7 @@ Save and send for approval Save list view Schedule + Preview Save and preview Preview is disabled because there's no template assigned Choose style @@ -168,6 +169,7 @@ Content sent for publishing Content sent for publishing for languages: %0% Sort child items performed by user + %0% Copy Publish Publish @@ -180,6 +182,7 @@ Send To Publish Send To Publish Sort + Custom History (all variants) @@ -684,6 +687,7 @@ Icon Id Import + Include subfolders in search Search only this folder Info Inner margin @@ -2226,6 +2230,12 @@ To manage your website, simply open the Umbraco back office and start adding con Open backoffice search Open/Close backoffice help Open/Close your profile options + Setup Culture and Hostnames for %0% + Create new node under %0% + Setup Public access on %0% + Setup Permissions on %0% + Change sort order for %0% + Create Content Template based on %0% Open context menu for Current language Switch language to @@ -2241,7 +2251,7 @@ To manage your website, simply open the Umbraco back office and start adding con Create Edit Name - Add new row + Add new row View more options @@ -2413,4 +2423,21 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + What are Content Templates? + Content Templates are pre-defined content that can be selected when creating a new content node. + How do I create a Content Template? + + There are two ways to create a Content Template:

+
    +
  • Right-click a content node and select "Create Content Template" to create a new Content Template.
  • +
  • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
  • +
+

Once given a name, editors can start using the Content Template as a foundation for their new page.

+ ]]> +
+ How do I manage Content Templates? + You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Expand the Document Type which the Content Template is based on and click it to edit or delete it. + 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 e4c764e112..2bb6495977 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -140,6 +140,7 @@ Send for approval Save list view Schedule + Preview Save and preview Preview is disabled because there's no template assigned Choose style @@ -170,6 +171,7 @@ Content sent for publishing Content sent for publishing for languages: %0% Sort child items performed by user + %0% Copy Publish Publish @@ -183,6 +185,7 @@ Send To Publish Send To Publish Sort + Custom History (all variants) @@ -367,7 +370,8 @@ Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type Document Type without a template + Edit permissions for this media type + Document Type without a template New folder New data type New JavaScript file @@ -568,6 +572,9 @@ #value or ?key=value Enter alias... Generating alias... + Create item + Edit + Name Create custom list view @@ -688,6 +695,7 @@ Icon Id Import + Include subfolders in search Search only this folder Info Inner margin @@ -2242,6 +2250,12 @@ To manage your website, simply open the Umbraco back office and start adding con Open backoffice search Open/Close backoffice help Open/Close your profile options + Setup Culture and Hostnames for %0% + Create new node under %0% + Setup Public access on %0% + Setup Permissions on %0% + Change sort order for %0% + Create Content Template based on %0% Open context menu for Current language Switch language to @@ -2429,4 +2443,21 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + What are Content Templates? + Content Templates are pre-defined content that can be selected when creating a new content node. + How do I create a Content Template? + + There are two ways to create a Content Template:

+
    +
  • Right-click a content node and select "Create Content Template" to create a new Content Template.
  • +
  • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
  • +
+

Once given a name, editors can start using the Content Template as a foundation for their new page.

+ ]]> +
+ How do I manage Content Templates? + You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Expand the Document Type which the Content Template is based on and click it to edit or delete it. + diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index 7b3f2a2184..3709e703d9 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -14,6 +14,10 @@ "content": "

Thank you for using Umbraco! Would you like to stay up-to-date with Umbraco product updates, security advisories, community news and special offers? Sign up for our newsletter and never miss out on the latest Umbraco news.

By signing up, you agree that we can use your info according to our privacy policy.

", "view": "emails", "type": "promotion" + }, + { + "title": "Thank you for subscribing to our mailing list", + "view": "confirm" } ] }, @@ -184,27 +188,21 @@ "event": "click" }, { - "element": "[data-element~='editor-data-type-picker']", + "element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']", "elementPreventClick": true, "title": "Editor picker", - "content": "

In the editor picker dialog we can pick one of the many built-in editors.

You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors).

" + "content": "

In the editor picker dialog we can pick one of the many built-in editors.

" }, { - "element": "[data-element~='editor-data-type-picker'] [data-element='editor-Textarea']", + "element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']", "title": "Select editor", "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.", "event": "click" }, { - "element": "[data-element~='editor-data-type-settings']", - "elementPreventClick": true, + "element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a", "title": "Editor settings", - "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed." - }, - { - "element": "[data-element~='editor-data-type-settings'] [data-element='button-submit']", - "title": "Save editor", - "content": "Click Submit to save the changes.", + "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.", "event": "click" }, { @@ -313,7 +311,8 @@ "content": "

To see all our templates click the small triangle to the left of the templates node.

", "event": "click", "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']", - "view": "templatetree" + "view": "templatetree", + "skipStepIfVisible": "#tree [data-element='tree-item-templates'] > div > button[data-element=tree-item-expand].icon-navigation-down" }, { "element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']", diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs new file mode 100644 index 0000000000..5d1aa8cb85 --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web.Compose +{ + public class NestedContentPropertyComponent : IComponent + { + public void Initialize() + { + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + } + + private void ContentService_Copying(IContentService sender, CopyEventArgs e) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + UpdateNestedContentProperties(nestedContentProps, false); + } + + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + // One or more content nodes could be saved in a bulk publish + foreach (var entity in e.SavedEntities) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + UpdateNestedContentProperties(nestedContentProps, true); + } + } + + public void Terminate() + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + } + + private void UpdateNestedContentProperties(IEnumerable nestedContentProps, bool onlyMissingKeys) + { + // Each NC Property on a doctype + foreach (var nestedContentProp in nestedContentProps) + { + // A NC Prop may have one or more values due to cultures + var propVals = nestedContentProp.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested NC's + var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested NC's + var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + + + // internal for tests + internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null) + { + // used so we can test nicely + if (createGuid == null) + createGuid = () => Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) + return rawJson; + + // Parse JSON + var complexEditorValue = JToken.Parse(rawJson); + + UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); + + return complexEditorValue.ToString(); + } + + private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func createGuid) + { + // check if this is NC + var isNestedContent = json.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false).Any(); + + // select all values (flatten) + var allProperties = json.SelectTokens("$..*").OfType().Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) + { + if (prop.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) + { + // get it's sibling 'key' property + var ncKeyVal = prop.Parent["key"] as JValue; + // TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys? + if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null)) + { + // create or replace + prop.Parent["key"] = createGuid().ToString(); + } + } + else if (!isNestedContent || prop.Name != "key") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested NC + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + { + // recurse + var parsed = JToken.Parse(propVal); + UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid); + // set the value to the updated one + prop.Value = parsed.ToString(); + } + } + } + } + + } +} diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs new file mode 100644 index 0000000000..4c9d9dee1c --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs @@ -0,0 +1,9 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Compose +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class NestedContentPropertyComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index fc34a35566..01e13ff051 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Collections.Generic; +using System.Net.Mail; using System.Security.Principal; using System.Threading.Tasks; using System.Web; @@ -47,6 +48,7 @@ namespace Umbraco.Web.Editors private readonly IHostingEnvironment _hostingEnvironment; private readonly IRuntimeState _runtimeState; private readonly ISecuritySettings _securitySettings; + private readonly IEmailSender _emailSender; public AuthenticationController( IUserPasswordConfiguration passwordConfiguration, @@ -60,13 +62,15 @@ namespace Umbraco.Web.Editors IRuntimeState runtimeState, UmbracoMapper umbracoMapper, ISecuritySettings securitySettings, - IPublishedUrlProvider publishedUrlProvider) + IPublishedUrlProvider publishedUrlProvider, + IEmailSender emailSender) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoMapper, publishedUrlProvider) { _passwordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); _securitySettings = securitySettings ?? throw new ArgumentNullException(nameof(securitySettings)); + _emailSender = emailSender; } protected BackOfficeUserManager UserManager => _userManager @@ -328,12 +332,19 @@ namespace Umbraco.Web.Editors UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings), new[] { identityUser.UserName, callbackUrl }); - // TODO: Port email service to ASP.NET Core - /*await UserManager.SendEmailAsync(identityUser.Id, - Services.TextService.Localize("login/resetPasswordEmailCopySubject", - // Ensure the culture of the found user is used for the email! - UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)), - message);*/ + var subject = Services.TextService.Localize("login/resetPasswordEmailCopySubject", + // Ensure the culture of the found user is used for the email! + UmbracoUserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)); + + var mailMessage = new MailMessage() + { + Subject = subject, + Body = message, + IsBodyHtml = true, + To = { user.Email} + }; + + await _emailSender.SendAsync(mailMessage); UserManager.RaiseForgotPasswordRequestedEvent(user.Id); } diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs index 04dcfab665..a2df34f569 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs @@ -51,7 +51,19 @@ namespace Umbraco.Web.Editors.Binders } } - // TODO: anything after 3 parts we can put in metadata + //if there are 4 parts part 4 is always segment + string segment = null; + if (parts.Length > 3) + { + segment = parts[3]; + //normalize to null if empty + if (segment.IsNullOrWhiteSpace()) + { + segment = null; + } + } + + // TODO: anything after 4 parts we can put in metadata var fileName = file.Headers.ContentDisposition.FileName.Trim('\"'); @@ -60,6 +72,7 @@ namespace Umbraco.Web.Editors.Binders TempFilePath = file.LocalFileName, PropertyAlias = propAlias, Culture = culture, + Segment = segment, FileName = fileName }); } diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index a7949084b5..b23cf57643 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -93,7 +93,7 @@ namespace Umbraco.Web.Editors // prepare files, if any matching property and culture var files = contentItem.UploadedFiles - .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture) + .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) .ToArray(); foreach (var file in files) diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 7aa71b6e2e..3c71245a0a 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mail; using System.Runtime.Serialization; using System.Security.Cryptography; using System.Threading.Tasks; @@ -52,6 +53,7 @@ namespace Umbraco.Web.Editors private readonly ISqlContext _sqlContext; private readonly IImageUrlGenerator _imageUrlGenerator; private readonly ISecuritySettings _securitySettings; + private readonly IEmailSender _emailSender; public UsersController( IGlobalSettings globalSettings, @@ -68,7 +70,8 @@ namespace Umbraco.Web.Editors IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator, IPublishedUrlProvider publishedUrlProvider, - ISecuritySettings securitySettings) + ISecuritySettings securitySettings, + IEmailSender emailSender) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) { _mediaFileSystem = mediaFileSystem; @@ -77,6 +80,7 @@ namespace Umbraco.Web.Editors _sqlContext = sqlContext; _imageUrlGenerator = imageUrlGenerator; _securitySettings = securitySettings; + _emailSender = emailSender; } /// @@ -500,17 +504,15 @@ namespace Umbraco.Web.Editors UmbracoUserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail }); - // TODO: Port email service to ASP.NET Core - /*await UserManager.EmailService.SendAsync( - //send the special UmbracoEmailMessage which configures it's own sender - //to allow for events to handle sending the message if no smtp is configured - new UmbracoEmailMessage(new EmailSender(GlobalSettings, true)) - { - Body = emailBody, - Destination = userDisplay.Email, - Subject = emailSubject - });*/ + var mailMessage = new MailMessage() + { + Subject = emailSubject, + Body = emailBody, + IsBodyHtml = true, + To = { to.Email} + }; + await _emailSender.SendAsync(mailMessage); } /// diff --git a/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs b/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs index 07a3f2a5e3..eb39cb8faa 100644 --- a/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs +++ b/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs @@ -25,7 +25,10 @@ namespace Umbraco.Web.Mvc //we have no choice but to instantiate the controller var instance = factory.CreateController(requestContext, controllerName); - return instance?.GetType(); + var controllerType = instance?.GetType(); + factory.ReleaseController(instance); + + return controllerType; } } } diff --git a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs index 98ba7f47de..cfc931fc71 100644 --- a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs @@ -82,7 +82,10 @@ namespace Umbraco.Web.Mvc //we have no choice but to instantiate the controller var instance = factory.CreateController(requestContext, controllerName); - return instance?.GetType(); + var controllerType = instance?.GetType(); + factory.ReleaseController(instance); + + return controllerType; } return GetControllerType(requestContext, controllerName); diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 5b59b632eb..5fee84e895 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -108,7 +108,6 @@ namespace Umbraco.Web.Runtime composition.WithCollectionBuilder() .Add(umbracoApiControllerTypes); - // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax composition.FilteredControllerFactory() diff --git a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs index dc849a3154..48a0f6b84c 100644 --- a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Routing; +using Umbraco.Core.Models; namespace Umbraco.Web.Trees { @@ -40,8 +41,6 @@ namespace Umbraco.Web.Trees protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { - //TODO: Do not allow deleting built in types - var menu = _menuItemCollectionFactory.Create(); if (id == Constants.System.RootString) @@ -58,7 +57,10 @@ namespace Umbraco.Web.Trees var relationType = Services.RelationService.GetRelationTypeById(int.Parse(id)); if (relationType == null) return menu; - menu.Items.Add(Services.TextService); + if (relationType.IsSystemRelationType() == false) + { + menu.Items.Add(Services.TextService); + } return menu; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9fb19447de..8a061987b1 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -144,6 +144,7 @@ + @@ -195,6 +196,7 @@ + diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index d95eb2c6e8..4261a87280 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -6,6 +6,8 @@ using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Runtime; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; @@ -32,9 +34,10 @@ namespace Umbraco.Web // Determine if we should use the sql main dom or the default var appSettingMainDomLock = globalSettings.MainDomLock; + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" || isLinux == true - ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connectionStrings, dbProviderFactoryCreator) + ? (IMainDomLock)new SqlMainDomLock(logger, globalSettings, connectionStrings, dbProviderFactoryCreator, hostingEnvironment) : new MainDomSemaphoreLock(logger, hostingEnvironment); var mainDom = new MainDom(logger, mainDomLock);