From ae54cb9947773a682d4992003dff8a27532202fa Mon Sep 17 00:00:00 2001 From: Chad Date: Wed, 4 May 2022 07:35:33 +1200 Subject: [PATCH 001/101] v10: Improve redirect content finder scalability (#12341) * Async support for content finders. Improve log performance. * Improve redirect contentfinder scalablilty with async calls to the database --- .../Repositories/IRedirectUrlRepository.cs | 18 ++++++- .../Routing/ContentFinderByRedirectUrl.cs | 2 +- .../Services/IRedirectUrlService.cs | 11 ++++- .../Services/RedirectUrlService.cs | 23 ++++++++- .../Implement/RedirectUrlRepository.cs | 49 +++++++++++++++++-- 5 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index 17be5b3856..0c2e4235ee 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Threading.Tasks; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories @@ -42,6 +43,13 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// The most recent redirect URL corresponding to the route. IRedirectUrl? GetMostRecentUrl(string url); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The most recent redirect URL corresponding to the route. + Task GetMostRecentUrlAsync(string url); + /// /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. /// @@ -50,6 +58,14 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// The most recent redirect URL corresponding to the route. IRedirectUrl? GetMostRecentUrl(string url, string culture); + /// + /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The culture the domain is associated with + /// The most recent redirect URL corresponding to the route. + Task GetMostRecentUrlAsync(string url, string culture); + /// /// Gets all redirect URLs for a content item. /// diff --git a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs index a200afec67..5b3d80f3ea 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByRedirectUrl.cs @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Core.Routing ? frequest.Domain.ContentId + DomainUtilities.PathRelativeToDomain(frequest.Domain.Uri, frequest.AbsolutePathDecoded) : frequest.AbsolutePathDecoded; - IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(route, frequest.Culture); + IRedirectUrl? redirectUrl = await _redirectUrlService.GetMostRecentRedirectUrlAsync(route, frequest.Culture); if (redirectUrl == null) { diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs index 3c061db466..ddefb2e23e 100644 --- a/src/Umbraco.Core/Services/IRedirectUrlService.cs +++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Threading.Tasks; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services @@ -56,6 +57,14 @@ namespace Umbraco.Cms.Core.Services /// The most recent redirect URLs corresponding to the route. IRedirectUrl? GetMostRecentRedirectUrl(string url, string? culture); + /// + /// Gets the most recent redirect URLs corresponding to an Umbraco redirect URL route. + /// + /// The Umbraco redirect URL route. + /// The culture of the request. + /// The most recent redirect URLs corresponding to the route. + Task GetMostRecentRedirectUrlAsync(string url, string? culture); + /// /// Gets all redirect URLs for a content item. /// diff --git a/src/Umbraco.Core/Services/RedirectUrlService.cs b/src/Umbraco.Core/Services/RedirectUrlService.cs index 14c3e834bf..8fe8b02a46 100644 --- a/src/Umbraco.Core/Services/RedirectUrlService.cs +++ b/src/Umbraco.Core/Services/RedirectUrlService.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; @@ -76,6 +77,13 @@ namespace Umbraco.Cms.Core.Services return _redirectUrlRepository.GetMostRecentUrl(url); } } + public async Task GetMostRecentRedirectUrlAsync(string url) + { + using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return await _redirectUrlRepository.GetMostRecentUrlAsync(url); + } + } public IEnumerable GetContentRedirectUrls(Guid contentKey) { @@ -118,5 +126,18 @@ namespace Umbraco.Cms.Core.Services } } + public async Task GetMostRecentRedirectUrlAsync(string url, string? culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return await GetMostRecentRedirectUrlAsync(url); + } + + using (var scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + return await _redirectUrlRepository.GetMostRecentUrlAsync(url, culture); + } + + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs index e49ccbdf77..52647983e0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -40,14 +41,27 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement public void Delete(Guid id) => Database.Delete(id); public IRedirectUrl? GetMostRecentUrl(string url) + { + Sql sql = GetMostRecentSql(url); + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + public async Task GetMostRecentUrlAsync(string url) + { + Sql sql = GetMostRecentSql(url); + List dtos = await Database.FetchAsync(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + private Sql GetMostRecentSql(string url) { var urlHash = url.GenerateHash(); Sql sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash) .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); - RedirectUrlDto? dto = dtos.FirstOrDefault(); - return dto == null ? null : Map(dto); + return sql; } public IRedirectUrl? GetMostRecentUrl(string url, string culture) @@ -57,12 +71,39 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return GetMostRecentUrl(url); } + Sql sql = GetMostRecentUrlSql(url, culture); + + List dtos = Database.Fetch(sql); + RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); + + if (dto == null) + { + dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture)); + } + + return dto == null ? null : Map(dto); + } + + private Sql GetMostRecentUrlSql(string url, string culture) + { var urlHash = url.GenerateHash(); Sql sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash && (x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty)) .OrderByDescending(x => x.CreateDateUtc); - List dtos = Database.Fetch(sql); + return sql; + } + + public async Task GetMostRecentUrlAsync(string url, string culture) + { + if (string.IsNullOrWhiteSpace(culture)) + { + return GetMostRecentUrl(url); + } + + Sql sql = GetMostRecentUrlSql(url, culture); + + List dtos = await Database.FetchAsync(sql); RedirectUrlDto? dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower()); if (dto == null) From 7993d19c1b4ba2a6303ef90e7acbfb51971003f2 Mon Sep 17 00:00:00 2001 From: Henk Jan Pluim Date: Mon, 25 Apr 2022 10:02:06 +0200 Subject: [PATCH 002/101] #fix 12254 return emptyresult --- src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs index 303ad44423..5f41682ee4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PreviewController.cs @@ -131,7 +131,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var user = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, "preview"); - return null; + return new EmptyResult(); } public ActionResult End(string? redir = null) From 9326cc5fc64a85e8e7fc19ae7faa726e33c33480 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Mon, 9 May 2022 11:42:10 +0200 Subject: [PATCH 003/101] Use SnippetCollection to when working with snippets (#12355) * Introducing a new Snippet type * Adding a SnippetCollection and SnippetCollectionBuilder * Using snippetCollection to get the snippets instead of fileService * Fixed fetching the correct content * Make ISnippet non-discoverable * Split the SnippetCollection into PartialViewSnippetCollection and PartialViewMacroSnippetCollection * Update CodeFileController to use the 2 snippet collections * Display the names with Empty.cshtml on top * Remove merging embedded snippets with custom snippets from ~\Umbraco.Web.UI\umbraco\PartialViewMacros\Templates folder for the Partial View Collection * Fix naming * Fix another naming * Cleanup + Use base items Co-authored-by: Bjarke Berg --- .../UmbracoBuilder.Collections.cs | 19 ++++- .../DependencyInjection/UmbracoBuilder.cs | 1 + src/Umbraco.Core/Services/IFileService.cs | 6 +- src/Umbraco.Core/Snippets/ISnippet.cs | 18 +++++ .../PartialViewMacroSnippetCollection.cs | 66 +++++++++++++++++ ...artialViewMacroSnippetCollectionBuilder.cs | 56 +++++++++++++++ .../Snippets/PartialViewSnippetCollection.cs | 70 +++++++++++++++++++ .../PartialViewSnippetCollectionBuilder.cs | 42 +++++++++++ src/Umbraco.Core/Snippets/Snippet.cs | 14 ++++ .../Controllers/CodeFileController.cs | 52 ++++++++++---- 10 files changed, 325 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Core/Snippets/ISnippet.cs create mode 100644 src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs create mode 100644 src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs create mode 100644 src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs create mode 100644 src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs create mode 100644 src/Umbraco.Core/Snippets/Snippet.cs diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index b1913037a3..77628d1953 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; @@ -10,11 +9,11 @@ using Umbraco.Cms.Core.HealthChecks.NotificationMethods; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Media.EmbedProviders; -using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Sections; +using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Tour; using Umbraco.Cms.Core.Trees; @@ -92,6 +91,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .Add() .Add() .Add(builder.TypeLoader.GetTypes()); + builder.PartialViewSnippets(); + builder.PartialViewMacroSnippets(); builder.DataValueReferenceFactories(); builder.PropertyValueConverters()?.Append(builder.TypeLoader.GetTypes()); builder.UrlSegmentProviders()?.Append(); @@ -202,6 +203,20 @@ namespace Umbraco.Cms.Core.DependencyInjection public static DashboardCollectionBuilder? Dashboards(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + /// + /// Gets the partial view snippets collection builder. + /// + /// The builder. + public static PartialViewSnippetCollectionBuilder? PartialViewSnippets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + + /// + /// Gets the partial view macro snippets collection builder. + /// + /// The builder. + public static PartialViewMacroSnippetCollectionBuilder? PartialViewMacroSnippets(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + /// /// Gets the cache refreshers collection builder. /// diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 4a05cb0268..327304fcc2 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -38,6 +38,7 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 6cbc06208c..baccd5dedf 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services @@ -10,6 +7,7 @@ namespace Umbraco.Cms.Core.Services /// public interface IFileService : IService { + [Obsolete("Please use SnippetCollection.GetPartialViewSnippetNames() or SnippetCollection.GetPartialViewMacroSnippetNames() instead. Scheduled for removal in V12.")] IEnumerable GetPartialViewSnippetNames(params string[] filterNames); void CreatePartialViewFolder(string folderPath); void CreatePartialViewMacroFolder(string folderPath); @@ -295,6 +293,7 @@ namespace Umbraco.Cms.Core.Services /// /// The name of the snippet /// + [Obsolete("Please use SnippetCollection.GetPartialViewMacroSnippetContent instead. Scheduled for removal in V12.")] string GetPartialViewMacroSnippetContent(string snippetName); /// @@ -302,6 +301,7 @@ namespace Umbraco.Cms.Core.Services /// /// The name of the snippet /// The content of the partial view. + [Obsolete("Please use SnippetCollection.GetPartialViewSnippetContent instead. Scheduled for removal in V12.")] string GetPartialViewSnippetContent(string snippetName); } } diff --git a/src/Umbraco.Core/Snippets/ISnippet.cs b/src/Umbraco.Core/Snippets/ISnippet.cs new file mode 100644 index 0000000000..67c9bf9e7f --- /dev/null +++ b/src/Umbraco.Core/Snippets/ISnippet.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// Defines a partial view macro snippet. + /// + public interface ISnippet + { + /// + /// Gets the name of the snippet. + /// + string Name { get; } + + /// + /// Gets the content of the snippet. + /// + string Content { get; } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs new file mode 100644 index 0000000000..b2fb79553c --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollection.cs @@ -0,0 +1,66 @@ +using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The collection of partial view macro snippets. + /// + public class PartialViewMacroSnippetCollection : BuilderCollectionBase + { + public PartialViewMacroSnippetCollection(Func> items) : base(items) + { + } + + /// + /// Gets the partial view macro snippet names. + /// + /// The names of all partial view macro snippets. + public IEnumerable GetNames() + { + var snippetNames = this.Select(x => Path.GetFileNameWithoutExtension(x.Name)).ToArray(); + + // Ensure the ones that are called 'Empty' are at the top + var empty = snippetNames.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false) + .OrderBy(x => x?.Length).ToArray(); + + return empty.Union(snippetNames.Except(empty)).WhereNotNull(); + } + + /// + /// Gets the content of a partial view macro snippet as a string. + /// + /// The name of the snippet. + /// The content of the partial view macro. + public string GetContentFromName(string snippetName) + { + if (snippetName.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(snippetName)); + } + + string partialViewMacroHeader = "@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage"; + + var snippet = this.Where(x => x.Name.Equals(snippetName + ".cshtml")).FirstOrDefault(); + + // Try and get the snippet path + if (snippet is null) + { + throw new InvalidOperationException("Could not load snippet with name " + snippetName); + } + + // Strip the @inherits if it's there + var snippetContent = StripPartialViewHeader(snippet.Content); + + var content = $"{partialViewMacroHeader}{Environment.NewLine}{snippetContent}"; + return content; + } + + private string StripPartialViewHeader(string contents) + { + var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline); + return headerMatch.Replace(contents, string.Empty); + } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs new file mode 100644 index 0000000000..cf737368ce --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewMacroSnippetCollectionBuilder.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Extensions; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The partial view macro snippet collection builder. + /// + public class PartialViewMacroSnippetCollectionBuilder : LazyCollectionBuilderBase + { + protected override PartialViewMacroSnippetCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) + { + var hostEnvironment = factory.GetRequiredService(); + + var embeddedSnippets = new List(base.CreateItems(factory)); + var snippetProvider = new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets"); + var embeddedFiles = snippetProvider.GetDirectoryContents(string.Empty) + .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml")); + + foreach (var file in embeddedFiles) + { + using var stream = new StreamReader(file.CreateReadStream()); + embeddedSnippets.Add(new Snippet(file.Name, stream.ReadToEnd().Trim())); + } + + var customSnippetsDir = new DirectoryInfo(hostEnvironment.MapPathContentRoot($"{Constants.SystemDirectories.Umbraco}/PartialViewMacros/Templates")); + if (!customSnippetsDir.Exists) + { + return embeddedSnippets; + } + + var customSnippets = customSnippetsDir.GetFiles().Select(f => new Snippet(f.Name, File.ReadAllText(f.FullName))); + var allSnippets = Merge(embeddedSnippets, customSnippets); + + return allSnippets; + } + + private IEnumerable Merge(IEnumerable embeddedSnippets, IEnumerable customSnippets) + { + var allSnippets = embeddedSnippets.Concat(customSnippets); + + var duplicates = allSnippets.GroupBy(s => s.Name) + .Where(gr => gr.Count() > 1) // Finds the snippets with the same name + .Select(s => s.First()); // Takes the first element from a grouping, which is the embeded snippet with that same name, + // since the physical snippet files are placed after the embedded ones in the all snippets colleciton + + // Remove any embedded snippets if a physical file with the same name can be found + return allSnippets.Except(duplicates); + } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs b/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs new file mode 100644 index 0000000000..5a0cda96e9 --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewSnippetCollection.cs @@ -0,0 +1,70 @@ +using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The collection of partial view snippets. + /// + public class PartialViewSnippetCollection : BuilderCollectionBase + { + public PartialViewSnippetCollection(Func> items) : base(items) + { + } + + /// + /// Gets the partial view snippet names. + /// + /// The names of all partial view snippets. + public IEnumerable GetNames() + { + var snippetNames = this.Select(x => Path.GetFileNameWithoutExtension(x.Name)).ToArray(); + + // Ensure the ones that are called 'Empty' are at the top + var empty = snippetNames.Where(x => Path.GetFileName(x)?.InvariantStartsWith("Empty") ?? false) + .OrderBy(x => x?.Length).ToArray(); + + return empty.Union(snippetNames.Except(empty)).WhereNotNull(); + } + + /// + /// Gets the content of a partial view snippet as a string. + /// + /// The name of the snippet. + /// The content of the partial view. + public string GetContentFromName(string snippetName) + { + if (snippetName.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(snippetName)); + } + + string partialViewHeader = "@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage"; + + var snippet = this.Where(x => x.Name.Equals(snippetName + ".cshtml")).FirstOrDefault(); + + // Try and get the snippet path + if (snippet is null) + { + throw new InvalidOperationException("Could not load snippet with name " + snippetName); + } + + var snippetContent = CleanUpContents(snippet.Content); + + var content = $"{partialViewHeader}{Environment.NewLine}{snippetContent}"; + return content; + } + + private string CleanUpContents(string content) + { + // Strip the @inherits if it's there + var headerMatch = new Regex("^@inherits\\s+?.*$", RegexOptions.Multiline); + var newContent = headerMatch.Replace(content, string.Empty); + + return newContent + .Replace("Model.Content.", "Model.") + .Replace("(Model.Content)", "(Model)"); + } + } +} diff --git a/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs b/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs new file mode 100644 index 0000000000..730e984d33 --- /dev/null +++ b/src/Umbraco.Core/Snippets/PartialViewSnippetCollectionBuilder.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Snippets +{ + /// + /// The partial view snippet collection builder. + /// + public class PartialViewSnippetCollectionBuilder : LazyCollectionBuilderBase + { + protected override PartialViewSnippetCollectionBuilder This => this; + + protected override IEnumerable CreateItems(IServiceProvider factory) + { + var embeddedSnippets = new List(base.CreateItems(factory)); + + // Ignore these + var filterNames = new List + { + "Gallery", + "ListChildPagesFromChangeableSource", + "ListChildPagesOrderedByProperty", + "ListImagesFromMediaFolder" + }; + + var snippetProvider = new EmbeddedFileProvider(typeof(IAssemblyProvider).Assembly, "Umbraco.Cms.Core.EmbeddedResources.Snippets"); + var embeddedFiles = snippetProvider.GetDirectoryContents(string.Empty) + .Where(x => !x.IsDirectory && x.Name.EndsWith(".cshtml")); + + foreach (var file in embeddedFiles) + { + if (!filterNames.Contains(Path.GetFileNameWithoutExtension(file.Name))) + { + using var stream = new StreamReader(file.CreateReadStream()); + embeddedSnippets.Add(new Snippet(file.Name, stream.ReadToEnd().Trim())); + } + } + + return embeddedSnippets; + } + } +} diff --git a/src/Umbraco.Core/Snippets/Snippet.cs b/src/Umbraco.Core/Snippets/Snippet.cs new file mode 100644 index 0000000000..bcb03b6d11 --- /dev/null +++ b/src/Umbraco.Core/Snippets/Snippet.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Core.Snippets +{ + public class Snippet : ISnippet + { + public string Name { get; } + public string Content { get; } + + public Snippet(string name, string content) + { + Name = name; + Content = content; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs index a302edc56b..fbcfe283ea 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/CodeFileController.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Net; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; @@ -15,6 +12,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Snippets; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Strings.Css; using Umbraco.Cms.Web.BackOffice.Filters; @@ -22,6 +20,7 @@ using Umbraco.Cms.Web.BackOffice.Trees; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; using Stylesheet = Umbraco.Cms.Core.Models.Stylesheet; @@ -45,7 +44,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IUmbracoMapper _umbracoMapper; private readonly IShortStringHelper _shortStringHelper; private readonly GlobalSettings _globalSettings; + private readonly PartialViewSnippetCollection _partialViewSnippetCollection; + private readonly PartialViewMacroSnippetCollection _partialViewMacroSnippetCollection; + [ActivatorUtilitiesConstructor] public CodeFileController( IHostingEnvironment hostingEnvironment, FileSystems fileSystems, @@ -54,7 +56,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers ILocalizedTextService localizedTextService, IUmbracoMapper umbracoMapper, IShortStringHelper shortStringHelper, - IOptionsSnapshot globalSettings) + IOptionsSnapshot globalSettings, + PartialViewSnippetCollection partialViewSnippetCollection, + PartialViewMacroSnippetCollection partialViewMacroSnippetCollection) { _hostingEnvironment = hostingEnvironment; _fileSystems = fileSystems; @@ -64,6 +68,31 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _umbracoMapper = umbracoMapper; _shortStringHelper = shortStringHelper; _globalSettings = globalSettings.Value; + _partialViewSnippetCollection = partialViewSnippetCollection; + _partialViewMacroSnippetCollection = partialViewMacroSnippetCollection; + } + + [Obsolete("Use ctor will all params. Scheduled for removal in V12.")] + public CodeFileController( + IHostingEnvironment hostingEnvironment, + FileSystems fileSystems, + IFileService fileService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILocalizedTextService localizedTextService, + IUmbracoMapper umbracoMapper, + IShortStringHelper shortStringHelper, + IOptionsSnapshot globalSettings) : this( + hostingEnvironment, + fileSystems, + fileService, + backOfficeSecurityAccessor, + localizedTextService, + umbracoMapper, + shortStringHelper, + globalSettings, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { } /// @@ -272,15 +301,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers switch (type) { case Constants.Trees.PartialViews: - snippets = _fileService.GetPartialViewSnippetNames( - //ignore these - (this is taken from the logic in "PartialView.ascx.cs") - "Gallery", - "ListChildPagesFromChangeableSource", - "ListChildPagesOrderedByProperty", - "ListImagesFromMediaFolder"); + snippets = _partialViewSnippetCollection.GetNames(); break; case Constants.Trees.PartialViewMacros: - snippets = _fileService.GetPartialViewSnippetNames(); + snippets = _partialViewMacroSnippetCollection.GetNames(); break; default: return NotFound(); @@ -312,7 +336,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers codeFileDisplay.VirtualPath = Constants.SystemDirectories.PartialViews; if (snippetName.IsNullOrWhiteSpace() == false) { - codeFileDisplay.Content = _fileService.GetPartialViewSnippetContent(snippetName!); + codeFileDisplay.Content = _partialViewSnippetCollection.GetContentFromName(snippetName!); } } @@ -324,7 +348,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers codeFileDisplay.VirtualPath = Constants.SystemDirectories.MacroPartials; if (snippetName.IsNullOrWhiteSpace() == false) { - codeFileDisplay.Content = _fileService.GetPartialViewMacroSnippetContent(snippetName!); + codeFileDisplay.Content = _partialViewMacroSnippetCollection.GetContentFromName(snippetName!); } } From ebb1dc21a91e08ad994bdd46ab2dd89ed0de3632 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Wed, 11 May 2022 00:51:37 +0200 Subject: [PATCH 004/101] Show nicer overlay when clicking block card for deleted element type (#12140) * Show nicer overlay when clicking block card for deleted element type * Cleanup * Remove stop-scrolling container * Use flex-start instead on start * Remove legacy flexbox fallback * Remove unnecessary hack * Use standard gap property instead * Localization of message * Fix translation * End sentence with a dot --- .../EmbeddedResources/Lang/da.xml | 7 +- .../EmbeddedResources/Lang/en.xml | 1 + .../EmbeddedResources/Lang/en_us.xml | 1 + .../src/common/services/editor.service.js | 2 - .../src/common/services/overlay.service.js | 2 +- .../src/less/components/editor.less | 12 +- src/Umbraco.Web.UI.Client/src/less/navs.less | 26 ++-- .../blockcard/umb-block-card-grid.less | 11 +- .../editor/umb-editor-container.html | 2 +- ...blocklist.blockconfiguration.controller.js | 124 ++++++++++-------- .../blocklist.blockconfiguration.html | 6 +- .../blocklist.blockconfiguration.less | 10 +- 12 files changed, 101 insertions(+), 103 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index ea28892f49..8717ba3bb0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -2181,18 +2181,19 @@ Mange hilsner fra Umbraco robotten være muligt. Indholdet vil blive vist som ikke understøttet indhold. + Kan ikke redigeres fordi elementtypen ikke eksisterer. Billede Tilføj billede Opret ny Udklipsholder Indstillinger Avanceret - Skjuld indholds editoren + Skjul indholdseditoren Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem? Annuller oprettelse? - Error! - The ElementType of this block does not exist anymore + Fejl! + Elementtypen for denne blok eksisterer ikke længere Tilføj indhold Tilføj %0% Feltet %0% bruger editor %1% som ikke er supporteret for blokke. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 1b93cacf01..1b7bcbf2d0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -2714,6 +2714,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont will no longer be available and will be shown as unsupported content. + Cannot be edited cause ElementType does not exist. Thumbnail Add thumbnail Create empty diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index b1dd754861..7ebce617b5 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -2802,6 +2802,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont will no longer be available and will be shown as unsupported content. + Cannot be edited cause ElementType does not exist. Thumbnail Add thumbnail Create empty diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index b146f471e0..cb95e60e29 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -165,13 +165,11 @@ When building a custom infinite editor view you can use the same components as a function editorService(eventsService, keyboardService, $timeout) { - let editorsKeyboardShorcuts = []; var editors = []; var isEnabled = true; var lastElementInFocus = null; - // events for backdrop eventsService.on("appState.backdrop", function (name, args) { if (args.show === true) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js index 113b26d74c..6e05712ad2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/overlay.service.js @@ -10,7 +10,7 @@ function overlayService(eventsService, backdropService, focusLockService) { - var currentOverlay = null; + let currentOverlay = null; /** * @ngdoc method diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index 4f71fe46da..211e4d4f34 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -202,17 +202,13 @@ input.umb-editor-header__name-input:disabled { bottom: 0; } -.umb-editor-container.-stop-scrolling { - overflow: hidden; -} - -.umb-editor-actions{ +.umb-editor-actions { list-style: none; margin: 0; padding: 0; -} -.umb-editor-actions li{ - display: inline-block; + li { + display: inline-block; + } } // editor footer diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index 3d5f5e40f5..db18d3f9d2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -266,19 +266,19 @@ .dropdown-menu > li > button { position: relative; - background: transparent; - border: 0; - padding: 8px 20px; - color: @ui-option-type; - display: flex; - justify-content: start; - clear: both; - font-weight: normal; - line-height: 20px; - white-space: nowrap; - cursor:pointer; - width: 100%; - text-align: left; + background: transparent; + border: 0; + padding: 8px 20px; + color: @ui-option-type; + display: flex; + justify-content: flex-start; + clear: both; + font-weight: normal; + line-height: 20px; + white-space: nowrap; + cursor:pointer; + width: 100%; + text-align: left; } .dropdown-menu > li > a:hover, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less index 58794461df..470eb3173d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less @@ -1,15 +1,6 @@ .umb-block-card-grid { - /* FlexBox Fallback */ - display: flex; - flex-wrap: wrap; - > * { - flex: 1 1 240px; - } - - /* Grid Setup */ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-auto-rows: minmax(160px, auto); - grid-gap: 20px; - + gap: 20px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html index 9e675b15ba..aa1765f365 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-container.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js index 5e73755593..f0b89a095f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -20,7 +20,7 @@ var unsubscribe = []; - var vm = this; + const vm = this; vm.openBlock = null; function onInit() { @@ -30,11 +30,10 @@ } loadElementTypes(); - } function loadElementTypes() { - return elementTypeResource.getAll().then(function (elementTypes) { + return elementTypeResource.getAll().then(elementTypes => { vm.elementTypes = elementTypes; }); } @@ -47,24 +46,32 @@ } } } + unsubscribe.push(eventsService.on("editors.documentType.saved", updateUsedElementTypes)); - vm.requestRemoveBlockByIndex = function (index) { - localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockTypeMessage", "blockEditor_confirmDeleteBlockTypeNotice"]).then(function (data) { + vm.requestRemoveBlockByIndex = function (index, event) { + + const labelKeys = [ + "general_delete", + "blockEditor_confirmDeleteBlockTypeMessage", + "blockEditor_confirmDeleteBlockTypeNotice" + ]; + + localizationService.localizeMany(labelKeys).then(data => { var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); overlayService.confirmDelete({ title: data[0], content: localizationService.tokenReplace(data[1], [contentElementType ? contentElementType.name : "(Unavailable ElementType)"]), confirmMessage: data[2], - close: function () { - overlayService.close(); - }, - submit: function () { + submit: () => { vm.removeBlockByIndex(index); overlayService.close(); - } + }, + close: overlayService.close() }); }); + + event.stopPropagation(); } vm.removeBlockByIndex = function (index) { @@ -78,7 +85,6 @@ placeholder: 'umb-block-card --sortable-placeholder' }; - vm.getAvailableElementTypes = function () { return vm.elementTypes.filter(function (type) { return !$scope.model.value.find(function (entry) { @@ -89,15 +95,13 @@ vm.getElementTypeByKey = function(key) { if (vm.elementTypes) { - return vm.elementTypes.find(function (type) { - return type.key === key; - }) || null; + return vm.elementTypes.find(type => type.key === key) || null; } }; vm.openAddDialog = function () { - localizationService.localize("blockEditor_headlineCreateBlock").then(function(localizedTitle) { + localizationService.localize("blockEditor_headlineCreateBlock").then(localizedTitle => { const contentTypePicker = { title: localizedTitle, @@ -108,10 +112,9 @@ filter: function (node) { if (node.metaData.isElement === true) { var key = udiService.getKey(node.udi); + // If a Block with this ElementType as content already exists, we will emit it as a posible option. - return $scope.model.value.find(function (entry) { - return key === entry.contentElementTypeKey; - }); + return $scope.model.value.find(entry => entry.contentElementTypeKey === key); } return true; }, @@ -138,8 +141,8 @@ } ] }; + editorService.treePicker(contentTypePicker); - }); }; @@ -151,9 +154,10 @@ isElement: true, noTemplate: true, submit: function (model) { - loadElementTypes().then( function () { + loadElementTypes().then(() => { callback(model.documentTypeKey); }); + editorService.close(); }, close: function () { @@ -165,60 +169,66 @@ vm.addBlockFromElementTypeKey = function(key) { - var blockType = { - "contentElementTypeKey": key, - "settingsElementTypeKey": null, - "labelTemplate": "", - "view": null, - "stylesheet": null, - "editorSize": "medium", - "iconColor": null, - "backgroundColor": null, - "thumbnail": null + const blockType = { + contentElementTypeKey: key, + settingsElementTypeKey: null, + labelTemplate: "", + view: null, + stylesheet: null, + editorSize: "medium", + iconColor: null, + backgroundColor: null, + thumbnail: null }; $scope.model.value.push(blockType); vm.openBlockOverlay(blockType); - }; - - - - vm.openBlockOverlay = function (block) { var elementType = vm.getElementTypeByKey(block.contentElementTypeKey); - if(elementType) { - localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [elementType.name]).then(function (data) { + if (elementType) { + + let clonedBlockData = Utilities.copy(block); + vm.openBlock = block; - var clonedBlockData = Utilities.copy(block); - vm.openBlock = block; - - var overlayModel = { - block: clonedBlockData, - title: data, - view: "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html", - size: "small", - submit: function(overlayModel) { - loadElementTypes()// lets load elementType again, to ensure we are up to date. - TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.) - overlayModel.close(); - }, - close: function() { - editorService.close(); - vm.openBlock = null; - } - }; + const overlayModel = { + block: clonedBlockData, + + view: "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html", + size: "small", + submit: function(overlayModel) { + loadElementTypes()// lets load elementType again, to ensure we are up to date. + TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.) + overlayModel.close(); + }, + close: function() { + editorService.close(); + vm.openBlock = null; + } + }; + localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [elementType.name]).then(data => { + overlayModel.title = data, + // open property settings editor editorService.open(overlayModel); - }); } else { - alert("Cannot be edited cause ElementType does not exist."); + + const overlay = { + close: () => { + overlayService.close() + } + }; + + localizationService.localize("blockEditor_elementTypeDoesNotExist").then(data => { + overlay.content = data; + overlayService.open(overlay); + }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html index de6a409415..3172219434 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -1,7 +1,7 @@
- +
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less index 878f6a8ef8..afb4316ce8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less @@ -18,11 +18,11 @@ padding: 5px 15px; box-sizing: border-box; font-weight: bold; - } - - .__add-button:hover { - color: @ui-action-discreet-type-hover; - border-color: @ui-action-discreet-border-hover; + + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } } } From 953e6f2e343da52950c20a981cf36deced1b5ffc Mon Sep 17 00:00:00 2001 From: Joe Glombek Date: Thu, 12 May 2022 11:19:44 +0100 Subject: [PATCH 005/101] Rework and update the contributing guide (#12165) --- .github/BUILD.md | 119 +++++++++-- .github/CONTRIBUTING.md | 320 +++++++++++++++------------- .github/CONTRIBUTION_GUIDELINES.md | 35 --- .github/REVIEW_PROCESS.md | 25 --- .github/img/tableofcontentsicon.svg | 3 + 5 files changed, 275 insertions(+), 227 deletions(-) delete mode 100644 .github/CONTRIBUTION_GUIDELINES.md delete mode 100644 .github/REVIEW_PROCESS.md create mode 100644 .github/img/tableofcontentsicon.svg diff --git a/.github/BUILD.md b/.github/BUILD.md index 5f962a8911..2c046b5396 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -4,30 +4,96 @@ In order to use Umbraco as a CMS and build your website with it, you should not build it yourself. If you're reading this then you're trying to contribute to Umbraco or you're debugging a complex issue. -- Are you about to create a pull request for Umbraco? +- Are you about to [create a pull request for Umbraco][contribution guidelines]? - Are you trying to get to the bottom of a problem in your existing Umbraco installation? If the answer is yes, please read on. Otherwise, make sure to head on over [to the download page](https://our.umbraco.com/download) and start using Umbraco CMS as intended. -**Table of contents** +## Table of contents -[Building from source](#building-from-source) - * [The quick build](#quick) - * [Build infrastructure](#build-infrastructure) - * [Properties](#properties) - * [GetUmbracoVersion](#getumbracoversion) - * [SetUmbracoVersion](#setumbracoversion) - * [Build](#build) - * [Build-UmbracoDocs](#build-umbracodocs) - * [Verify-NuGet](#verify-nuget) - * [Cleaning up](#cleaning-up) +↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above. -[Azure DevOps](#azure-devops) -[Quirks](#quirks) - * [Powershell quirks](#powershell-quirks) - * [Git quirks](#git-quirks) +## Debugging source locally +Did you read ["Are you sure"](#are-you-sure)? + +[More details about contributing to Umbraco and how to use the GitHub tooling can be found in our guide to contributing.][contribution guidelines] + +If you want to run a build without debugging, see [Building from source](#building-from-source) below. This runs the build in the same way it is run on our build servers. + +#### Debugging with VS Code + +In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed. + + * [Visual Studio Code](https://code.visualstudio.com/) + * [dotnet SDK v5.0](https://dotnet.microsoft.com/en-us/download) + * [Node.js v10+](https://nodejs.org/en/download/) + * npm v6.4.1+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) + +Open the root folder of the repository in Code. + +To build the front end you'll need to open the command pallet (Ctrl + Shift + P) and run `>Tasks: Run Task` followed by `Client Install` and then run the `Client Build` task in the same way. + +You can also run the tasks manually on the command line: + +``` +cd src\Umbraco.Web.UI.Client +npm install +npm run watch +``` + +or + +``` +cd src\Umbraco.Web.UI.Client +npm install +gulp watch +``` + +**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.** + +You might run into [Gulp quirks](#gulp-quirks). + +The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making. + +To run the C# portion of the project, either hit F5 to begin debugging, or manually using the command line: + +``` +dotnet watch --project .\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj +``` + +**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.** + +When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging. + +#### Debugging with Visual Studio + +In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed. + + * [Visual Studio 2019 v16.8+ with .NET 5+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects) + * [Node.js v10+](https://nodejs.org/en/download/) + * npm v6.4.1+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) + +The easiest way to get started is to open `umbraco.sln` in Visual Studio. + +To build the front end, you'll first need to run `cd src\Umbraco.Web.UI.Client && npm install` in the command line (or `cd src\Umbraco.Web.UI.Client; npm install` in PowerShell). Then find the Task Runner Explorer (View → Other Windows → Task Runner Explorer) and run the `build` task under `Gulpfile.js`. You may need to refresh the Task Runner Explorer before the tasks load. + +If you're working on the backoffice, you may wish to run the `dev` command instead while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. + +**The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.** + +You might run into [Gulp quirks](#gulp-quirks). + +The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to [disable caching in the browser (check "Disable cache" on the "Network" tab of developer tools)][disable browser caching] to help you to see the changes you're making. + +"The rest" is a C# based codebase, which is mostly ASP.NET Core MVC based. You can make changes, build them in Visual Studio, and hit F5 to see the result. + +**The initial C# build might take a _really_ long time (seriously, go and make a cup of coffee!) - but don't worry, this will be faster on subsequent runs.** + +When the page eventually loads in your web browser, you can follow the installer to set up a database for debugging. You may also wish to install a [starter kit][starter kits] to ease your debugging. ## Building from source @@ -38,13 +104,14 @@ Did you read ["Are you sure"](#are-you-sure)? To build Umbraco, fire up PowerShell and move to Umbraco's repository root (the directory that contains `src`, `build`, `LICENSE.md`...). There, trigger the build with the following command: build/build.ps1 - + If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (v8/contrib) the file will appear and you can build it. You might run into [Powershell quirks](#powershell-quirks). If it runs without errors; Hooray! Now you can continue with [the next step](CONTRIBUTING.md#how-do-i-begin) and open the solution and build it. + ### Build Infrastructure The Umbraco Build infrastructure relies on a PowerShell object. The object can be retrieved with: @@ -145,7 +212,7 @@ To perform a more complete clear, you will want to also delete the content of th The following command will force remove all untracked files and directories, whether they are ignored by Git or not. Combined with `git reset` it can recreate a pristine working directory. git clean -xdf . - + For git documentation see: * git [clean]() * git [reset]() @@ -214,3 +281,19 @@ The best solution is to unblock the Zip file before un-zipping: right-click the ### Git Quirks Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details). + +### Gulp Quirks + +You may need to run the following commands to set up gulp properly: + + ``` +npm cache clean --force +npm ci +npm run build + ``` + + + +[ contribution guidelines]: CONTRIBUTING.md "Read the guide to contributing for more details on contributing to Umbraco" +[ starter kits ]: https://our.umbraco.com/packages/?category=Starter%20Kits&version=9 "Browse starter kits available for v9 on Our " +[ disable browser caching ]: https://techwiser.com/disable-cache-google-chrome-firefox "Instructions on how to disable browser caching in Chrome and Firefox" diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e8b378fb15..0f5d8a7752 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,192 +2,117 @@ 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 -The following is a set of guidelines, for contributing to Umbraco CMS. +These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! 💖 Use your best judgement, and feel free to propose changes to this document in a pull request. -These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. +## Coding not your thing? Or want more ways to contribute? -Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you 💖. +This document covers contributing to the codebase of the CMS but [the community site has plenty of inspiration for other ways to get involved.][get involved] -**Code of conduct** +If you don't feel you'd like to make code changes here, you can visit our [documentation repository][docs repo] and use your experience to contribute to making the docs we have, even better. -This project and everyone participating in it, is governed by the [our Code of Conduct](https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md). +We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Collaborators and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. -**Table of contents** +## Table of contents -[Contributing code changes](#contributing-code-changes) - * [Guidelines for contributions we welcome](#guidelines-for-contributions-we-welcome) - * [Ownership and copyright](#ownership-and-copyright) - * [What can I start with?](#what-can-i-start-with) - * [How do I begin?](#how-do-i-begin) - * [Pull requests](#pull-requests) +↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above. -[Reviews](#reviews) - * [Styleguides](#styleguides) - * [The Core Contributors](#the-core-contributors-team) - * [Questions?](#questions) -[Working with the code](#working-with-the-code) - * [Building Umbraco from source code](#building-umbraco-from-source-code) - * [Working with the source code](#working-with-the-source-code) - * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) - * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) - * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) +## Before you start -## Contributing code changes -This document gives you a quick overview on how to get started. +### Code of Conduct -### Guidelines for contributions we welcome +This project and everyone participating in it, is governed by the [our Code of Conduct][code of conduct]. -Not all changes are wanted, so on occasion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. +### What can I contribute? -We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes, so we can ensure that you don't put all your hard work into something we would not be able to merge. +We categorise pull requests (PRs) into two categories: -Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. +| PR type | Definition | +| --------- | ------------------------------------------------------------ | +| Small PRs | Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. | +| Large PRs | New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). | + +We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process][review process]. + +We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. + +Not all changes are wanted, so on occasion we might close a PR without merging it but if we do, we will give you feedback why we can't accept your changes. **So make sure to [talk to us before making large changes][making larger changes]**, so we can ensure that you don't put all your hard work into something we would not be able to merge. + +#### Making larger changes + +[making larger changes]: #making-larger-changes + +Please make sure to describe your larger ideas in an [issue (bugs)][issues] or [discussion (new features)][discussions], it helps to put in mock up screenshots or videos. If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. + +If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the small PRs process above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. + +#### Pull request or package? + +[pr or package]: #pull-request-or-package + +If you're unsure about whether your changes belong in the core Umbraco CMS or if you should turn your idea into a package instead, make sure to [talk to us][making larger changes]. + +If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. Eventually, a package could "graduate" to be included in the CMS. #### Ownership and copyright -It is your responsibility to make sure that you're allowed to share the code you're providing us. -For example, you should have permission from your employer or customer to share code. +It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code. Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS. If you're not sure, leave a note on your contribution and we will be happy to guide you. -When your contribution has been accepted, it will be [MIT licensed](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/LICENSE.md) from that time onwards. +When your contribution has been accepted, it will be [MIT licensed][MIT license] from that time onwards. -### What can I start with? +## Finding your first issue: Up for grabs -Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) +Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. -### How do I begin? +If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. + +## Making your changes Great question! The short version goes like this: - * **Fork** - create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS) +1. **Fork** - ![Fork the repository](img/forkrepository.png) + Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo] + + ![Fork the repository](img/forkrepository.png) + +1. **Clone** - * **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool + When GitHub has created your fork, you can clone it in your favorite Git tool + + ![Clone the fork](img/clonefork.png) + +1. **Switch to the correct branch** - ![Clone the fork](img/clonefork.png) + Switch to the `v10/contrib` branch - * **Switch to the correct branch** - switch to the `v9/contrib` branch - * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md) - * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) - * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v9/contrib`, create a new branch first. - * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. +1. **Build** - ![Create a pull request](img/createpullrequest.png) + Build your fork of Umbraco locally as described in the build documentation: you can [debug with Visual Studio Code][build - debugging with code] or [with Visual Studio][build - debugging with vs]. -### Pull requests -The most successful pull requests usually look a like this: +1. **Branch** - * Fill in the required template (shown when starting a PR on GitHub), and link your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. - * Include screenshots and animated GIFs in your pull request whenever possible. - * Unit tests, while optional, are awesome. Thank you! - * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated. + Create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case issue number `12345`. Don't commit to `v10/contrib`, create a new branch first. -Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work! +1. **Change** -## Reviews + Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback][questions]. -You've sent us your first contribution - congratulations! Now what? +1. **Commit and push** -The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes. + Done? Yay! 🎉 -We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is: + Remember to commit to your new `temp` branch, and don't commit to `v10/contrib`. Then you can push the changes up to your fork on GitHub. -- Your PR will get a reply within 48 hours -- An in-depth reply will be added within at most 2 weeks -- The PR will be either merged or rejected within at most 4 weeks -- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case. +#### Keeping your Umbraco fork in sync with the main repository +[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository -### Styleguides - -To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up. - -That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. - -### The Core Contributors team - -The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: - -- [Nathan Woulfe](https://github.com/nathanwoulfe) -- [Joe Glombek](https://github.com/glombek) -- [Laura Weatherhead](https://github.com/lssweatherhead) -- [Michael Latouche](https://github.com/mikecp) -- [Owain Williams](https://github.com/OwainWilliams) - - -These wonderful people aim to provide you with a first reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. Hq will have final sign-off and will check the work again before it is merged. - -### Questions? - -You can get in touch with [the core contributors team](#the-core-contributors-team) in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: - -- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward. -- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. - -## Working with the code - -### Building Umbraco from source code - -In order to build the Umbraco source code locally, first make sure you have the following installed. - - * [Visual Studio 2019 v16.8+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/) - * [Node.js v10+](https://nodejs.org/en/download/) - * npm v6.4.1+ (installed with Node.js) - * [Git command line](https://git-scm.com/download/) - -The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2019 (version 16.3 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. - -Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. - -![Gulp build in Visual Studio](img/gulpbuild.png) - -After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser. Follow the directions there to get a working Umbraco install up and running. - -### Working with the source code - -Some parts of our source code are over 10 years old now. And when we say "old", we mean "mature" of course! - -There are two big areas that you should know about: - - 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. - You may need to run the following commands to set up gulp properly: - ``` - npm cache clean --force - npm ci - npm run build - ``` - The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making. - - 2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result. - -To find the general areas for something you're looking to fix or improve, have a look at the following two parts of the API documentation. - - * [The AngularJS based backoffice files](https://apidocs.umbraco.com/v9/ui#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) - * [The C# application](https://apidocs.umbraco.com/v9/csharp/) - -### Which branch should I target for my contributions? - -We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v9/contrib`. If you are working on v9, this is the branch you should be targetting. For v8 contributions, please target 'v8/contrib' - -Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise. - -![Which branch should I target?](img/defaultbranch.png) - -### Making changes after the PR is open - -If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco! - -### Keeping your Umbraco fork in sync with the main repository - -We recommend you to sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. - -Also, if you have submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. +Once you've already got a fork and cloned your fork locally, you can skip steps 1 and 2 going forward. Just remember to keep your fork up to date before making further changes. To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once: @@ -199,13 +124,110 @@ Then when you want to get the changes from the main repository: ``` git fetch upstream -git rebase upstream/v9/contrib +git rebase upstream/v10/contrib ``` -In this command we're syncing with the `v9/contrib` branch, but you can of course choose another one if needed. +In this command we're syncing with the `v10/contrib` branch, but you can of course choose another one if needed. -(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) +[More information on how this works can be found on the thoughtbot blog.][sync fork ext] -### And finally +#### Style guide -We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Contributors and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. +To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up. + +That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. + +#### Questions? +[questions]: #questions + +You can get in touch with [the core contributors team][core collabs] in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: + +- If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward. +- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs]. +- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. + +## Creating a pull request + +Exciting! You're ready to show us your changes. + +We recommend you to [sync with our repository][sync fork] before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. + +GitHub will have picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. +![Create a pull request](img/createpullrequest.png) + +We like to use [git flow][git flow] as much as possible, but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v10/contrib`. If you are working on v9, this is the branch you should be targeting. + +Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise. + +## The review process +[review process]: #the-review-process + +You've sent us your first contribution - congratulations! Now what? + +The [Core Collaborators team][Core collabs] can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes. + +You will get an initial automated reply from our [Friendly Umbraco Robot, Umbrabot][Umbrabot], to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. You can take this opportunity to double check everything is in order based off the handy checklist Umbrabot provides. + +You will get feedback as soon as the [Core Collaborators team][Core collabs] can after opening the PR. You’ll most likely get feedback within a couple of weeks. Then there are a few possible outcomes: + +- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco +- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible +- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see +- Your proposed change is awesome but... not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!). See [making larger changes][making larger changes] and [pull request or package?][pr or package] + +### Dealing with requested changes + +If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco! + +#### No longer available? + +We understand you have other things to do and can't just drop everything to help us out. + +So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. + +If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. + +There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. + +### The Core Collaborators team +[Core collabs]: #the-core-collaborators-team + +The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time: + +- [Nathan Woulfe][Nathan Woulfe] +- [Joe Glombek][Joe Glombek] +- [Laura Weatherhead][Laura Weatherhead] +- [Michael Latouche][Michael Latouche] +- [Owain Williams][Owain Williams] + + +These wonderful people aim to provide you with a reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. HQ will have final sign-off and will check the work again before it is merged. + + + + + +[MIT license]: ../LICENSE.md "Umbraco's license declaration" +[build - debugging with vs]: BUILD.md#debugging-with-visual-studio "Details on building and debugging Umbraco with Visual Studio" +[build - debugging with code]: BUILD.md#debugging-with-vs-code "Details on building and debugging Umbraco with Visual Studio Code" + + + +[Nathan Woulfe]: https://github.com/nathanwoulfe "Nathan's GitHub profile" +[Joe Glombek]: https://github.com/glombek "Joe's GitHub profile" +[Laura Weatherhead]: https://github.com/lssweatherhead "Laura's GitHub profile" +[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile" +[Owain Williams]: https://github.com/OwainWilliams "Owain's GitHub profile" +[Sebastiaan]: https://github.com/nul800sebastiaan "Senastiaan's GitHub profile" +[ Umbrabot ]: https://github.com/umbrabot +[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow" +[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated" +[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests" +[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/ +[get involved]: https://community.umbraco.com/get-involved/ +[docs repo]: https://github.com/umbraco/UmbracoDocs +[code of conduct]: https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md +[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs +[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS +[issues]: https://github.com/umbraco/Umbraco-CMS/issues +[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions \ No newline at end of file diff --git a/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md deleted file mode 100644 index 0ac35e6897..0000000000 --- a/.github/CONTRIBUTION_GUIDELINES.md +++ /dev/null @@ -1,35 +0,0 @@ -# Contributing to Umbraco CMS - -When you’re considering creating a pull request for Umbraco CMS, we will categorize them in two different sizes, small and large. - -The process for both sizes is very similar, as [explained in the contribution document](CONTRIBUTING.md#how-do-i-begin). - -## Small PRs -Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. - -We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process](REVIEW_PROCESS.md). - -### Up for grabs - -Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. - -If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. - -## Large PRs -New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). - -We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. - -Please make sure to describe your idea in an issue, it helps to put in mockup screenshots or videos. - -If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. - -If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the [small PRs process](#small-prs) above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. - -It is highly recommended that you speak to the HQ before making large, complex changes. - -### Pull request or package? - -If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. - -Eventually, a package could "graduate" to be included in the CMS. diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md deleted file mode 100644 index 917d25b090..0000000000 --- a/.github/REVIEW_PROCESS.md +++ /dev/null @@ -1,25 +0,0 @@ -# Review process - -You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response. - -In order to set some expectations, here's what happens next. - -## Review process - -You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. - -You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes: - -- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco -- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible -- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see -- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!) - -## Are you still available? - -We understand you have other things to do and can't just drop everything to help us out. -So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. - -If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. - -There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. \ No newline at end of file diff --git a/.github/img/tableofcontentsicon.svg b/.github/img/tableofcontentsicon.svg new file mode 100644 index 0000000000..a08c8742d3 --- /dev/null +++ b/.github/img/tableofcontentsicon.svg @@ -0,0 +1,3 @@ + \ No newline at end of file From 2c34751fd41d59d6c6b9edf7808ed7fe59d8418a Mon Sep 17 00:00:00 2001 From: failureflawless <42579327+failureflawless@users.noreply.github.com> Date: Thu, 12 May 2022 11:45:04 +0000 Subject: [PATCH 006/101] Updated docs and VSCode build task as we discovered some changes we needed in the docs --- .github/BUILD.md | 10 +++++----- .vscode/tasks.json | 12 ++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/BUILD.md b/.github/BUILD.md index 2c046b5396..070bccb0b2 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -27,21 +27,21 @@ If you want to run a build without debugging, see [Building from source](#buildi In order to build the Umbraco source code locally with Visual Studio Code, first make sure you have the following installed. * [Visual Studio Code](https://code.visualstudio.com/) - * [dotnet SDK v5.0](https://dotnet.microsoft.com/en-us/download) + * [dotnet SDK v6.0](https://dotnet.microsoft.com/en-us/download) * [Node.js v10+](https://nodejs.org/en/download/) * npm v6.4.1+ (installed with Node.js) * [Git command line](https://git-scm.com/download/) -Open the root folder of the repository in Code. +Open the root folder of the repository in Visual Studio Code. -To build the front end you'll need to open the command pallet (Ctrl + Shift + P) and run `>Tasks: Run Task` followed by `Client Install` and then run the `Client Build` task in the same way. +To build the front end you'll need to open the command pallet (Ctrl + Shift + P) and run `>Tasks: Run Task` followed by `Client Watch` and then run the `Client Build` task in the same way. You can also run the tasks manually on the command line: ``` cd src\Umbraco.Web.UI.Client npm install -npm run watch +npm run dev ``` or @@ -49,7 +49,7 @@ or ``` cd src\Umbraco.Web.UI.Client npm install -gulp watch +gulp dev ``` **The initial Gulp build might take a long time - don't worry, this will be faster on subsequent runs.** diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1d4324a34d..99876bc77e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -33,6 +33,18 @@ "$gulp-tsc" ] }, + { + "label": "Client Watch", + "detail": "runs npm run dev for Umbraco.Web.UI.Client", + "promptOnClose": true, + "group": "build", + "type": "npm", + "script": "dev", + "path": "src/Umbraco.Web.UI.Client/", + "problemMatcher": [ + "$gulp-tsc" + ] + }, { "label": "Dotnet build", "detail": "Dotnet build of SLN", From 898c5cc3973f907099466e559754c67516ebfe37 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 16 May 2022 13:08:33 +0200 Subject: [PATCH 007/101] Update BUILD.md --- .github/BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/BUILD.md b/.github/BUILD.md index 070bccb0b2..28a8ea810a 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -28,8 +28,8 @@ In order to build the Umbraco source code locally with Visual Studio Code, first * [Visual Studio Code](https://code.visualstudio.com/) * [dotnet SDK v6.0](https://dotnet.microsoft.com/en-us/download) - * [Node.js v10+](https://nodejs.org/en/download/) - * npm v6.4.1+ (installed with Node.js) + * [Node.js v14+](https://nodejs.org/en/download/) + * npm v7+ (installed with Node.js) * [Git command line](https://git-scm.com/download/) Open the root folder of the repository in Visual Studio Code. From 048193e3a5cb48e19da5425b66bb8b32415e8106 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 16 May 2022 13:12:08 +0200 Subject: [PATCH 008/101] Update BUILD.md --- .github/BUILD.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/BUILD.md b/.github/BUILD.md index 28a8ea810a..2ec254de6c 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -73,8 +73,8 @@ When the page eventually loads in your web browser, you can follow the installer In order to build the Umbraco source code locally with Visual Studio, first make sure you have the following installed. * [Visual Studio 2019 v16.8+ with .NET 5+](https://visualstudio.microsoft.com/vs/) ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects) - * [Node.js v10+](https://nodejs.org/en/download/) - * npm v6.4.1+ (installed with Node.js) + * [Node.js v14+](https://nodejs.org/en/download/) + * npm v7+ (installed with Node.js) * [Git command line](https://git-scm.com/download/) The easiest way to get started is to open `umbraco.sln` in Visual Studio. From d051f850eb3be5c0bbbd5d2fab2cd35c5b6aae1c Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 16 May 2022 23:50:19 +0200 Subject: [PATCH 009/101] Support SVG icon in action menu (#12403) * Support custom SVG icon in menu item with legacy support * Update menu icons * Update action icons * Adjust icons in menu actions with legacy fallback * Don't use legacy icon * Update comments --- .../Actions/ActionAssignDomain.cs | 2 +- src/Umbraco.Core/Actions/ActionCopy.cs | 4 +- .../ActionCreateBlueprintFromContent.cs | 4 +- src/Umbraco.Core/Actions/ActionDelete.cs | 4 +- src/Umbraco.Core/Actions/ActionMove.cs | 4 +- src/Umbraco.Core/Actions/ActionNew.cs | 4 +- src/Umbraco.Core/Actions/ActionNotify.cs | 4 +- src/Umbraco.Core/Actions/ActionProtect.cs | 4 +- src/Umbraco.Core/Actions/ActionRestore.cs | 4 +- src/Umbraco.Core/Actions/ActionRights.cs | 4 +- src/Umbraco.Core/Actions/ActionRollback.cs | 4 +- src/Umbraco.Core/Actions/ActionSort.cs | 4 +- src/Umbraco.Core/Actions/ActionToPublish.cs | 4 +- src/Umbraco.Core/Actions/ActionUnpublish.cs | 4 +- src/Umbraco.Core/Actions/ActionUpdate.cs | 4 +- .../Models/Trees/CreateChildEntity.cs | 13 ++++-- src/Umbraco.Core/Models/Trees/ExportMember.cs | 5 ++- src/Umbraco.Core/Models/Trees/MenuItem.cs | 23 ++++++---- src/Umbraco.Core/Models/Trees/RefreshNode.cs | 12 ++++-- src/Umbraco.Core/Trees/MenuItemList.cs | 10 +++-- .../Trees/ContentBlueprintTreeController.cs | 6 +-- .../Trees/ContentTreeController.cs | 37 ++++++++-------- .../Trees/ContentTreeControllerBase.cs | 7 +++- .../Trees/ContentTypeTreeController.cs | 41 ++++++++++-------- .../Trees/DataTypeTreeController.cs | 20 +++++---- .../Trees/DictionaryTreeController.cs | 7 ++-- .../Trees/FileSystemTreeController.cs | 42 ++++++++++++------- .../Trees/MacrosTreeController.cs | 10 ++--- .../Trees/MediaTreeController.cs | 30 ++++++------- .../Trees/MediaTypeTreeController.cs | 19 +++++---- .../Trees/MemberGroupTreeController.cs | 1 - .../Trees/MemberTreeController.cs | 18 ++++---- .../MemberTypeAndGroupTreeControllerBase.cs | 4 +- .../Trees/RelationTypeTreeController.cs | 11 +++-- .../Trees/TemplatesTreeController.cs | 15 ++++--- .../application/umbcontextmenu.directive.js | 21 ++++++---- .../editor/umbeditormenu.directive.js | 14 ++++--- .../application/umb-contextmenu.html | 2 +- .../components/editor/umb-editor-menu.html | 7 ++-- 39 files changed, 244 insertions(+), 189 deletions(-) diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 0638f605af..b975cf65af 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -23,7 +23,7 @@ public class ActionAssignDomain : IAction public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; /// - public string Icon => "home"; + public string Icon => "icon-home"; /// public bool ShowInNotifier => false; diff --git a/src/Umbraco.Core/Actions/ActionCopy.cs b/src/Umbraco.Core/Actions/ActionCopy.cs index 83a855d1ff..f097388214 100644 --- a/src/Umbraco.Core/Actions/ActionCopy.cs +++ b/src/Umbraco.Core/Actions/ActionCopy.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.StructureCategory; /// - public string Icon => "documents"; + public string Icon => "icon-documents"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs index 806868af40..a3b827141e 100644 --- a/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs +++ b/src/Umbraco.Core/Actions/ActionCreateBlueprintFromContent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -18,7 +18,7 @@ namespace Umbraco.Cms.Core.Actions public bool CanBePermissionAssigned => true; /// - public string Icon => "blueprint"; + public string Icon => Constants.Icons.Blueprint; /// public string Alias => "createblueprint"; diff --git a/src/Umbraco.Core/Actions/ActionDelete.cs b/src/Umbraco.Core/Actions/ActionDelete.cs index b31a8b9c45..88e79d472a 100644 --- a/src/Umbraco.Core/Actions/ActionDelete.cs +++ b/src/Umbraco.Core/Actions/ActionDelete.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -28,7 +28,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.ContentCategory; /// - public string Icon => "delete"; + public string Icon => "icon-delete"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionMove.cs b/src/Umbraco.Core/Actions/ActionMove.cs index 0f8b4b8305..404cfa7d36 100644 --- a/src/Umbraco.Core/Actions/ActionMove.cs +++ b/src/Umbraco.Core/Actions/ActionMove.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.StructureCategory; /// - public string Icon => "enter"; + public string Icon => "icon-enter"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionNew.cs b/src/Umbraco.Core/Actions/ActionNew.cs index 25e85cd377..6a3412c9cc 100644 --- a/src/Umbraco.Core/Actions/ActionNew.cs +++ b/src/Umbraco.Core/Actions/ActionNew.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Core.Actions public string Alias => ActionAlias; /// - public string Icon => "add"; + public string Icon => "icon-add"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionNotify.cs b/src/Umbraco.Core/Actions/ActionNotify.cs index 3f1e855cff..4ec2f17138 100644 --- a/src/Umbraco.Core/Actions/ActionNotify.cs +++ b/src/Umbraco.Core/Actions/ActionNotify.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -18,7 +18,7 @@ namespace Umbraco.Cms.Core.Actions public bool CanBePermissionAssigned => true; /// - public string Icon => "megaphone"; + public string Icon => "icon-megaphone"; /// public string Alias => "notify"; diff --git a/src/Umbraco.Core/Actions/ActionProtect.cs b/src/Umbraco.Core/Actions/ActionProtect.cs index 10684a69e2..d6e3e798ff 100644 --- a/src/Umbraco.Core/Actions/ActionProtect.cs +++ b/src/Umbraco.Core/Actions/ActionProtect.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; /// - public string Icon => "lock"; + public string Icon => "icon-lock"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionRestore.cs b/src/Umbraco.Core/Actions/ActionRestore.cs index 164c93e2d5..a58d244401 100644 --- a/src/Umbraco.Core/Actions/ActionRestore.cs +++ b/src/Umbraco.Core/Actions/ActionRestore.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string? Category => null; /// - public string Icon => "undo"; + public string Icon => "icon-undo"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionRights.cs b/src/Umbraco.Core/Actions/ActionRights.cs index fff7cc8652..a186abfa06 100644 --- a/src/Umbraco.Core/Actions/ActionRights.cs +++ b/src/Umbraco.Core/Actions/ActionRights.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.ContentCategory; /// - public string Icon => "vcard"; + public string Icon => "icon-vcard"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionRollback.cs b/src/Umbraco.Core/Actions/ActionRollback.cs index 565a8469c5..46498e419e 100644 --- a/src/Umbraco.Core/Actions/ActionRollback.cs +++ b/src/Umbraco.Core/Actions/ActionRollback.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; /// - public string Icon => "undo"; + public string Icon => "icon-undo"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionSort.cs b/src/Umbraco.Core/Actions/ActionSort.cs index 1f87bfcc3c..3a46281a46 100644 --- a/src/Umbraco.Core/Actions/ActionSort.cs +++ b/src/Umbraco.Core/Actions/ActionSort.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.StructureCategory; /// - public string Icon => "navigation-vertical"; + public string Icon => "icon-navigation-vertical"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionToPublish.cs b/src/Umbraco.Core/Actions/ActionToPublish.cs index 654b71661d..f2627edbc3 100644 --- a/src/Umbraco.Core/Actions/ActionToPublish.cs +++ b/src/Umbraco.Core/Actions/ActionToPublish.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.ContentCategory; /// - public string Icon => "outbox"; + public string Icon => "icon-outbox"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Actions/ActionUnpublish.cs b/src/Umbraco.Core/Actions/ActionUnpublish.cs index 6e9ec8506b..c79e1b934a 100644 --- a/src/Umbraco.Core/Actions/ActionUnpublish.cs +++ b/src/Umbraco.Core/Actions/ActionUnpublish.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.ContentCategory; /// - public string Icon => "circle-dotted"; + public string Icon => "icon-circle-dotted"; /// public bool ShowInNotifier => false; diff --git a/src/Umbraco.Core/Actions/ActionUpdate.cs b/src/Umbraco.Core/Actions/ActionUpdate.cs index 3f8092c1fc..18b35d805e 100644 --- a/src/Umbraco.Core/Actions/ActionUpdate.cs +++ b/src/Umbraco.Core/Actions/ActionUpdate.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions @@ -23,7 +23,7 @@ namespace Umbraco.Cms.Core.Actions public string Category => Constants.Conventions.PermissionCategories.ContentCategory; /// - public string Icon => "save"; + public string Icon => "icon-save"; /// public bool ShowInNotifier => true; diff --git a/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs b/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs index a8d945242e..e0b695dded 100644 --- a/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs +++ b/src/Umbraco.Core/Models/Trees/CreateChildEntity.cs @@ -1,27 +1,32 @@ -using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Models.Trees { /// - /// Represents the refresh node menu item + /// Represents the refresh node menu item. /// public sealed class CreateChildEntity : ActionMenuItem { + private const string icon = "icon-add"; + public override string AngularServiceName => "umbracoMenuActions"; public CreateChildEntity(string name, bool separatorBefore = false) : base(ActionNew.ActionAlias, name) { - Icon = "add"; Name = name; + Icon = icon; + Name = name; SeparatorBefore = separatorBefore; + UseLegacyIcon = false; } public CreateChildEntity(ILocalizedTextService textService, bool separatorBefore = false) : base(ActionNew.ActionAlias, textService) { - Icon = "add"; + Icon = icon; SeparatorBefore = separatorBefore; + UseLegacyIcon = false; } } } diff --git a/src/Umbraco.Core/Models/Trees/ExportMember.cs b/src/Umbraco.Core/Models/Trees/ExportMember.cs index 30f904f952..cbaa95498b 100644 --- a/src/Umbraco.Core/Models/Trees/ExportMember.cs +++ b/src/Umbraco.Core/Models/Trees/ExportMember.cs @@ -1,9 +1,9 @@ -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Models.Trees { /// - /// Represents the export member menu item + /// Represents the export member menu item. /// public sealed class ExportMember : ActionMenuItem { @@ -12,6 +12,7 @@ namespace Umbraco.Cms.Core.Models.Trees public ExportMember(ILocalizedTextService textService) : base("export", textService) { Icon = "download-alt"; + UseLegacyIcon = false; } } } diff --git a/src/Umbraco.Core/Models/Trees/MenuItem.cs b/src/Umbraco.Core/Models/Trees/MenuItem.cs index e56a2440a8..f6c3b6b4ac 100644 --- a/src/Umbraco.Core/Models/Trees/MenuItem.cs +++ b/src/Umbraco.Core/Models/Trees/MenuItem.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using System.Threading; @@ -10,7 +10,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models.Trees { /// - /// A context menu item + /// A context menu item. /// [DataContract(Name = "menuItem", Namespace = "")] public class MenuItem @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Models.Trees public MenuItem() { AdditionalData = new Dictionary(); - Icon = "folder"; + Icon = Constants.Icons.Folder; } public MenuItem(string alias, string name) @@ -38,7 +38,7 @@ namespace Umbraco.Cms.Core.Models.Trees } /// - /// Create a menu item based on an definition + /// Create a menu item based on an definition. /// /// /// @@ -80,16 +80,25 @@ namespace Umbraco.Cms.Core.Models.Trees public string? TextDescription { get; set; } /// - /// Ensures a menu separator will exist before this menu item + /// Ensures a menu separator will exist before this menu item. /// [DataMember(Name = "separator")] public bool SeparatorBefore { get; set; } - [DataMember(Name = "cssclass")] + /// + /// Icon to use at action menu item. + /// + [DataMember(Name = "icon")] public string Icon { get; set; } + /// Used in the UI to indicate whether icons should be prefixed with "icon-". + /// If not legacy icon full icon name should be specified. + ///
+ [DataMember(Name = "useLegacyIcon")] + public bool UseLegacyIcon { get; set; } = true; + /// - /// Used in the UI to inform the user that the menu item will open a dialog/confirmation + /// Used in the UI to inform the user that the menu item will open a dialog/confirmation. /// [DataMember(Name = "opensDialog")] public bool OpensDialog { get; set; } diff --git a/src/Umbraco.Core/Models/Trees/RefreshNode.cs b/src/Umbraco.Core/Models/Trees/RefreshNode.cs index 01eb2fa34a..207ec4f6ed 100644 --- a/src/Umbraco.Core/Models/Trees/RefreshNode.cs +++ b/src/Umbraco.Core/Models/Trees/RefreshNode.cs @@ -1,27 +1,31 @@ -using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Models.Trees { /// /// - /// Represents the refresh node menu item + /// Represents the refresh node menu item. /// public sealed class RefreshNode : ActionMenuItem { + private const string icon = "icon-refresh"; + public override string AngularServiceName => "umbracoMenuActions"; public RefreshNode(string name, bool separatorBefore = false) : base("refreshNode", name) { - Icon = "refresh"; + Icon = icon; SeparatorBefore = separatorBefore; + UseLegacyIcon = false; } public RefreshNode(ILocalizedTextService textService, bool separatorBefore = false) : base("refreshNode", textService) { - Icon = "refresh"; + Icon = icon; SeparatorBefore = separatorBefore; + UseLegacyIcon = false; } } } diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index b3fe420602..65a1535f89 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -38,10 +38,11 @@ namespace Umbraco.Cms.Core.Trees /// /// The used to localize the action name based on its alias /// Whether or not this action opens a dialog - public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) + /// Whether or not this action should use legacy icon prefixed with "icon-" or full icon name is specified. + public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false, bool useLegacyIcon = true) where T : IAction { - var item = CreateMenuItem(textService, hasSeparator, opensDialog); + var item = CreateMenuItem(textService, hasSeparator, opensDialog, useLegacyIcon); if (item != null) { Add(item); @@ -50,7 +51,7 @@ namespace Umbraco.Cms.Core.Trees return null; } - private MenuItem? CreateMenuItem(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) + private MenuItem? CreateMenuItem(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false, bool useLegacyIcon = true) where T : IAction { var item = _actionCollection.GetAction(); @@ -59,11 +60,12 @@ namespace Umbraco.Cms.Core.Trees 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)) + var menuItem = new MenuItem(item, textService.Localize("actions", item.Alias)) { SeparatorBefore = hasSeparator, OpensDialog = opensDialog, TextDescription = textDescription, + UseLegacyIcon = useLegacyIcon, }; return menuItem; diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs index 15f5839f30..02229e1348 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentBlueprintTreeController.cs @@ -135,7 +135,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { // root actions - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; } @@ -145,7 +145,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (cte != null) { var ct = _contentTypeService.Get(cte.Id); - var createItem = menu.Items.Add(LocalizedTextService, opensDialog: true); + var createItem = menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); createItem?.NavigateToRoute("/settings/contentBlueprints/edit/-1?create=true&doctype=" + ct?.Alias); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); @@ -153,7 +153,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees return menu; } - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); return menu; } diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 13e8a40e39..4b61ef2e17 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -169,8 +169,8 @@ namespace Umbraco.Cms.Web.BackOffice.Trees .Select(x => new MenuItem(x)); //these two are the standard items - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); //filter the standard items FilterUserAllowedMenuItems(menu, nodeActions); @@ -271,23 +271,24 @@ namespace Umbraco.Cms.Web.BackOffice.Trees protected MenuItemCollection GetAllNodeMenuItems(IUmbracoEntity item) { var menu = _menuItemCollectionFactory.Create(); - AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, true, opensDialog: true); - AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, true, opensDialog: true); - AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, opensDialog: true); - AddActionNode(item, menu, true, opensDialog: true); + AddActionNode(item, menu, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, true, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, true, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, opensDialog: true, useLegacyIcon: false); + AddActionNode(item, menu, true, opensDialog: true, useLegacyIcon: false); if (_emailSender.CanSendRequiredEmail()) { menu.Items.Add(new MenuItem("notify", LocalizedTextService) { - Icon = "megaphone", + Icon = "icon-megaphone", SeparatorBefore = true, - OpensDialog = true + OpensDialog = true, + UseLegacyIcon = false, }); } @@ -307,9 +308,9 @@ namespace Umbraco.Cms.Web.BackOffice.Trees protected MenuItemCollection GetNodeMenuItemsForDeletedContent(IUmbracoEntity item) { var menu = _menuItemCollectionFactory.Create(); - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); @@ -361,10 +362,10 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } } - private void AddActionNode(IUmbracoEntity item, MenuItemCollection menu, bool hasSeparator = false, bool opensDialog = false) + private void AddActionNode(IUmbracoEntity item, MenuItemCollection menu, bool hasSeparator = false, bool opensDialog = false, bool useLegacyIcon = true) where TAction : IAction { - var menuItem = menu.Items.Add(LocalizedTextService, hasSeparator, opensDialog); + var menuItem = menu.Items.Add(LocalizedTextService, hasSeparator, opensDialog, useLegacyIcon); } public async Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null) diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs index f0e62f8a66..3dd11e01dd 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeControllerBase.cs @@ -471,14 +471,17 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } var menu = MenuItemCollectionFactory.Create(); + // only add empty recycle bin if the current user is allowed to delete by default if (deleteAllowed) { menu.Items.Add(new MenuItem("emptyrecyclebin", LocalizedTextService) { - Icon = "trash", - OpensDialog = true + Icon = "icon-trash", + OpensDialog = true, + UseLegacyIcon = false, }); + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); } return menu; diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index aea6d8ab42..7aa0e54500 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -119,17 +119,19 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { - //set the default to create + // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; // root actions - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new MenuItem("importdocumenttype", LocalizedTextService) { - Icon = "page-up", + Icon = "icon-page-up", SeparatorBefore = true, - OpensDialog = true + OpensDialog = true, + UseLegacyIcon = false, }); + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -138,21 +140,23 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var container = _entityService.Get(int.Parse(id, CultureInfo.InvariantCulture), UmbracoObjectTypes.DocumentTypeContainer); if (container != null) { - //set the default to create + // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new MenuItem("rename", LocalizedTextService) { - Icon = "icon icon-edit" + Icon = "icon-edit", + UseLegacyIcon = false, }); if (container.HasChildren == false) { - //can delete doc type - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + // can delete doc type + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); } + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); } else @@ -160,22 +164,25 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var ct = _contentTypeService.Get(int.Parse(id, CultureInfo.InvariantCulture)); var parent = ct == null ? null : _contentTypeService.Get(ct.ParentId); - menu.Items.Add(LocalizedTextService, opensDialog: true); - //no move action if this is a child doc type + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + + // no move action if this is a child doc type if (parent == null) { - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); } - menu.Items.Add(LocalizedTextService, opensDialog: true); + + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new MenuItem("export", LocalizedTextService) { - Icon = "download-alt", + Icon = "icon-download-alt", SeparatorBefore = true, - OpensDialog = true + OpensDialog = true, + UseLegacyIcon = false, }); - menu.Items.Add(LocalizedTextService, true, opensDialog: true); - menu.Items.Add(new RefreshNode(LocalizedTextService, true)); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); } return menu; diff --git a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs index 574540e428..d3ad102c5a 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DataTypeTreeController.cs @@ -133,7 +133,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees menu.DefaultMenuAlias = ActionNew.ActionAlias; // root actions - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; } @@ -141,21 +141,23 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var container = _entityService.Get(int.Parse(id, CultureInfo.InvariantCulture), UmbracoObjectTypes.DataTypeContainer); if (container != null) { - //set the default to create + // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new MenuItem("rename", LocalizedTextService.Localize("actions", "rename")) { - Icon = "icon icon-edit" + Icon = "icon-edit", + UseLegacyIcon = false, }); if (container.HasChildren == false) { - //can delete data type - menu.Items.Add(LocalizedTextService, opensDialog: true); + // can delete data type + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); } + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); } else @@ -163,9 +165,11 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var nonDeletableSystemDataTypeIds = GetNonDeletableSystemDataTypeIds(); if (nonDeletableSystemDataTypeIds.Contains(int.Parse(id, CultureInfo.InvariantCulture)) == false) - menu.Items.Add(LocalizedTextService, opensDialog: true); + { + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + } - menu.Items.Add(LocalizedTextService, hasSeparator: true, opensDialog: true); + menu.Items.Add(LocalizedTextService, hasSeparator: true, opensDialog: true, useLegacyIcon: false); } return menu; diff --git a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs index 2e97769ca5..d559b87d87 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -126,15 +126,14 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { var menu = _menuItemCollectionFactory.Create(); - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); if (id != Constants.System.RootString) { - menu.Items.Add(LocalizedTextService, true, opensDialog: true); - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); } - menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; diff --git a/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs index ba32715f59..559c4561ef 100644 --- a/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/FileSystemTreeController.cs @@ -65,10 +65,14 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var hasChildren = FileSystem is not null && (FileSystem.GetFiles(directory).Any() || FileSystem.GetDirectories(directory).Any()); var name = Path.GetFileName(directory); - var node = CreateTreeNode(WebUtility.UrlEncode(directory), path, queryStrings, name, "icon-folder", hasChildren); + var node = CreateTreeNode(WebUtility.UrlEncode(directory), path, queryStrings, name, Constants.Icons.Folder, hasChildren); + OnRenderFolderNode(ref node); + if (node != null) + { nodes.Add(node); + } } } @@ -94,9 +98,13 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var name = Path.GetFileName(file); var node = CreateTreeNode(WebUtility.UrlEncode(file), path, queryStrings, name, FileIcon, false); + OnRenderFileNode(ref node); + if (node != null) + { nodes.Add(node); + } } } @@ -112,8 +120,9 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } var root = rootResult.Value; - //check if there are any children + // check if there are any children var treeNodesResult = GetTreeNodes(Constants.System.RootString, queryStrings); + if (!(treeNodesResult.Result is null)) { return treeNodesResult.Result; @@ -132,11 +141,13 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { var menu = MenuItemCollectionFactory.Create(); - //set the default to create + // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; - //create action - menu.Items.Add(LocalizedTextService, opensDialog: true); - //refresh action + + // create action + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + + // refresh action menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -146,21 +157,22 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { var menu = MenuItemCollectionFactory.Create(); - //set the default to create + // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; - //create action - menu.Items.Add(LocalizedTextService, opensDialog: true); + + // create action + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); var hasChildren = FileSystem is not null && (FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any()); - //We can only delete folders if it doesn't have any children (folders or files) + // We can only delete folders if it doesn't have any children (folders or files) if (hasChildren == false) { - //delete action - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + // delete action + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); } - //refresh action + // refresh action menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -170,8 +182,8 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { var menu = MenuItemCollectionFactory.Create(); - //if it's not a directory then we only allow to delete the item - menu.Items.Add(LocalizedTextService, opensDialog: true); + // if it's not a directory then we only allow to delete the item + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); return menu; } diff --git a/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs index 4edff226f6..c7870929f5 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MacrosTreeController.cs @@ -75,10 +75,10 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { - //Create the normal create action - menu.Items.Add(LocalizedTextService); + // Create the normal create action + menu.Items.Add(LocalizedTextService, useLegacyIcon: false); - //refresh action + // refresh action menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -87,8 +87,8 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var macro = _macroService.GetById(int.Parse(id, CultureInfo.InvariantCulture)); if (macro == null) return menu; - //add delete option for all macros - menu.Items.Add(LocalizedTextService, opensDialog: true); + // add delete option for all macros + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); return menu; } diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs index 2bd171b7c7..1afe16ea7b 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTreeController.cs @@ -118,9 +118,10 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } // root actions - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); + return menu; } @@ -134,7 +135,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees return NotFound(); } - //if the user has no path access for this node, all they can do is refresh + // if the user has no path access for this node, all they can do is refresh if (!_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasMediaPathAccess(item, _entityService, _appCaches) ?? false) { menu.Items.Add(new RefreshNode(LocalizedTextService, true)); @@ -142,27 +143,26 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } - //if the media item is in the recycle bin, we don't have a default menu and we need to show a limited menu + // if the media item is in the recycle bin, we don't have a default menu and we need to show a limited menu if (item.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); menu.DefaultMenuAlias = null; - } else { - //return a normal node menu: - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(LocalizedTextService); + // return a normal node menu: + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); - //set the default to create + // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; } @@ -172,7 +172,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees protected override UmbracoObjectTypes UmbracoObjectType => UmbracoObjectTypes.Media; /// - /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access + /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access. /// /// /// diff --git a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs index 94b0ae76a5..b70cc3dc60 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MediaTypeTreeController.cs @@ -96,7 +96,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees menu.DefaultMenuAlias = ActionNew.ActionAlias; // root actions - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService)); return menu; } @@ -107,17 +107,18 @@ namespace Umbraco.Cms.Web.BackOffice.Trees // set the default to create menu.DefaultMenuAlias = ActionNew.ActionAlias; - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new MenuItem("rename", LocalizedTextService.Localize("actions", "rename")) { - Icon = "icon icon-edit" + Icon = "icon-edit", + UseLegacyIcon = false }); if (container.HasChildren == false) { // can delete doc type - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); } menu.Items.Add(new RefreshNode(LocalizedTextService, true)); } @@ -126,21 +127,21 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var ct = _mediaTypeService.Get(int.Parse(id, CultureInfo.InvariantCulture)); var parent = ct == null ? null : _mediaTypeService.Get(ct.ParentId); - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); // no move action if this is a child doc type if (parent == null) { - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); } - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); if(ct?.IsSystemMediaType() == false) { - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); } - menu.Items.Add(new RefreshNode(LocalizedTextService, true)); + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); } return menu; diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index bd5c22b147..cb1114c24d 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -53,7 +53,6 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } - protected override IEnumerable GetTreeNodesFromService(string id, FormCollection queryStrings) => _memberGroupService.GetAll() .OrderBy(x => x.Name) diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs index 5cf469ded5..3c522763f6 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTreeController.cs @@ -96,7 +96,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees member.Name, Constants.Icons.Member, false, - "", + string.Empty, Udi.Create(ObjectTypes.GetUdiType(Constants.ObjectTypes.Member), member.Key)); node.AdditionalData.Add("contentType", member.ContentTypeAlias); @@ -121,9 +121,10 @@ namespace Umbraco.Cms.Web.BackOffice.Trees queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } - //There is no menu for any of these nodes + // There is no menu for any of these nodes nodes.ForEach(x => x.MenuUrl = null); - //All nodes are containers + + // All nodes are containers nodes.ForEach(x => x.AdditionalData.Add("isContainer", true)); return nodes; @@ -136,18 +137,19 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { // root actions - //set default + // set default menu.DefaultMenuAlias = ActionNew.ActionAlias; - //Create the normal create action - menu.Items.Add(LocalizedTextService, opensDialog: true); + // Create the normal create action + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); menu.Items.Add(new RefreshNode(LocalizedTextService, true)); + return menu; } - //add delete option for all members - menu.Items.Add(LocalizedTextService, opensDialog: true); + // add delete option for all members + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); if (_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.HasAccessToSensitiveData() ?? false) { diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs index 64e8081dec..8982f2d2f5 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs @@ -80,11 +80,11 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var memberType = _memberTypeService.Get(int.Parse(id)); if (memberType != null) { - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); } // delete member type/group - menu.Items.Add(LocalizedTextService, opensDialog: true); + menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); } return menu; diff --git a/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs index 22f96fa7c8..b9f34551c2 100644 --- a/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/RelationTypeTreeController.cs @@ -43,10 +43,10 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { - //Create the normal create action - menu.Items.Add(LocalizedTextService); + // Create the normal create action + menu.Items.Add(LocalizedTextService, useLegacyIcon: false); - //refresh action + // refresh action menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -57,7 +57,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (relationType.IsSystemRelationType() == false) { - menu.Items.Add(LocalizedTextService); + menu.Items.Add(LocalizedTextService, useLegacyIcon: false); } return menu; @@ -70,8 +70,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id == Constants.System.RootString) { nodes.AddRange(_relationService.GetAllRelationTypes() - .Select(rt => CreateTreeNode(rt.Id.ToString(), id, queryStrings, rt.Name, - "icon-trafic", false))); + .Select(rt => CreateTreeNode(rt.Id.ToString(), id, queryStrings, rt.Name, "icon-trafic", false))); } return nodes; diff --git a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs index caf976e7f2..b42312e634 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TemplatesTreeController.cs @@ -111,13 +111,13 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { var menu = _menuItemCollectionFactory.Create(); - //Create the normal create action - var item = menu.Items.Add(LocalizedTextService, opensDialog: true); + // Create the normal create action + var item = menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); item?.NavigateToRoute($"{queryStrings.GetRequiredValue("application")}/templates/edit/{id}?create=true"); if (id == Constants.System.RootString) { - //refresh action + // refresh action menu.Items.Add(new RefreshNode(LocalizedTextService, true)); return menu; @@ -127,17 +127,16 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (template == null) return menu; var entity = FromTemplate(template); - //don't allow delete if it has child layouts + // don't allow delete if it has child layouts if (template.IsMasterTemplate == false) { - //add delete option if it doesn't have children - menu.Items.Add(LocalizedTextService, true, opensDialog: true); + // add delete option if it doesn't have children + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); } - //add refresh + // add refresh menu.Items.Add(new RefreshNode(LocalizedTextService, true)); - return menu; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js index f666e62587..7749dacdcc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbcontextmenu.directive.js @@ -7,7 +7,7 @@ * Handles the click events on the context menu **/ angular.module("umbraco.directives") -.directive('umbContextMenu', function (navigationService, keyboardService, backdropService) { +.directive('umbContextMenu', function (navigationService, keyboardService) { return { scope: { menuDialogTitle: "@", @@ -20,22 +20,27 @@ angular.module("umbraco.directives") templateUrl: 'views/components/application/umb-contextmenu.html', link: function (scope, element, attrs, ctrl) { + // Map action icons using legacy icon font or svg icons. + Utilities.forEach(scope.menuActions, action => { + action.icon = (action.useLegacyIcon ? 'icon-' : '') + action.icon; + }); + //adds a handler to the context menu item click, we need to handle this differently //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { + scope.executeMenuItem = action => { navigationService.executeMenuAction(action, scope.currentNode, scope.currentSection); }; - - scope.outSideClick = function() { + + scope.outSideClick = () => { navigationService.hideNavigation(); }; - - keyboardService.bind("esc", function() { + + keyboardService.bind("esc", () => { navigationService.hideNavigation(); }); - + //ensure to unregister from all events! - scope.$on('$destroy', function () { + scope.$on('$destroy', () => { keyboardService.unbind("esc"); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js index 281a52e4e5..37ce9096f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditormenu.directive.js @@ -1,7 +1,7 @@ (function () { 'use strict'; - function EditorMenuDirective($injector, treeService, navigationService, umbModelMapper, appState) { + function EditorMenuDirective(treeService, navigationService, appState) { function link(scope, el, attr, ctrl) { @@ -17,7 +17,7 @@ //adds a handler to the context menu item click, we need to handle this differently //depending on what the menu item is supposed to do. - scope.executeMenuItem = function (action) { + scope.executeMenuItem = action => { //the action is called as it would be by the tree. to ensure that the action targets the correct node, //we need to set the current node in appState before calling the action. otherwise we break all actions //that use the current node (and that's pretty much all of them) @@ -34,10 +34,14 @@ } if (!scope.actions) { - treeService.getMenu({ treeNode: scope.currentNode }) - .then(function (data) { - scope.actions = data.menuItems; + treeService.getMenu({ treeNode: scope.currentNode }).then(data => { + scope.actions = data.menuItems; + + // Map action icons using legacy icon font or svg icons. + Utilities.forEach(scope.actions, action => { + action.icon = (action.useLegacyIcon ? 'icon-' : '') + action.icon; }); + }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 107103b1b4..cee9da9ebc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -14,7 +14,7 @@ ng-class="{sep:action.separator, '-opens-dialog': action.opensDialog}" ng-repeat="action in menuActions"> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html index f5ed572fa6..aa6ec85bc3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -8,14 +8,13 @@ show-caret="true" has-popup="true" is-expanded="dropdown.isOpen" - disabled="!actions || !actions.length || isDisabled" - > + disabled="!actions || !actions.length || isDisabled"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 48385a3cd4..1eacc386df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -32,10 +32,10 @@ var unsubscribe = []; var modelObject; - + // Property actions: - var copyAllBlocksAction = null; - var deleteAllBlocksAction = null; + let copyAllBlocksAction = null; + let deleteAllBlocksAction = null; var inlineEditing = false; var liveEditing = true; @@ -112,21 +112,23 @@ } else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) { scopeOfExistence = vm.umbElementEditorContent.getScope(); } - + copyAllBlocksAction = { labelKey: "clipboard_labelForCopyAllEntries", labelTokens: [vm.model.label], - icon: "documents", + icon: "icon-documents", method: requestCopyAllBlocks, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; deleteAllBlocksAction = { - labelKey: 'clipboard_labelForRemoveAllEntries', + labelKey: "clipboard_labelForRemoveAllEntries", labelTokens: [], - icon: 'trash', + icon: "icon-trash", method: requestDeleteAllBlocks, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; var propertyActions = [ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index d2a1710e49..15aa3ac973 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -117,12 +117,13 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso } }; - var removeAllEntriesAction = { - labelKey: 'clipboard_labelForRemoveAllEntries', + let removeAllEntriesAction = { + labelKey: "clipboard_labelForRemoveAllEntries", labelTokens: [], - icon: 'trash', + icon: "icon-trash", method: removeAllEntries, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; if ($scope.model.config) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index c7c803fa9a..2d479eb0c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -3,7 +3,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService, clipboardService) { - var vm = this; + const vm = this; vm.labels = {}; vm.labels.deletedItem = ""; @@ -341,20 +341,22 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }); } - var copyAllEntriesAction = { - labelKey: 'clipboard_labelForCopyAllEntries', + let copyAllEntriesAction = { + labelKey: "clipboard_labelForCopyAllEntries", labelTokens: ['Media'], - icon: "documents", + icon: "icon-documents", method: copyAllEntries, - isDisabled: true + isDisabled: true, + useLegacyIcon: false } - var removeAllEntriesAction = { - labelKey: 'clipboard_labelForRemoveAllEntries', + let removeAllEntriesAction = { + labelKey: "clipboard_labelForRemoveAllEntries", labelTokens: [], - icon: 'trash', + icon: "icon-trash", method: removeAllEntries, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; if (multiPicker === true) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js index 2699fa479b..57b94c9aa9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -33,8 +33,8 @@ var unsubscribe = []; // Property actions: - var copyAllMediasAction = null; - var removeAllMediasAction = null; + let copyAllMediasAction = null; + let removeAllMediasAction = null; var vm = this; @@ -69,17 +69,19 @@ copyAllMediasAction = { labelKey: "clipboard_labelForCopyAllEntries", labelTokens: [vm.model.label], - icon: "documents", + icon: "icon-documents", method: requestCopyAllMedias, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; removeAllMediasAction = { - labelKey: 'clipboard_labelForRemoveAllEntries', + labelKey: "clipboard_labelForRemoveAllEntries", labelTokens: [], - icon: 'trash', + icon: "icon-trash", method: requestRemoveAllMedia, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; var propertyActions = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js index fee3853351..f08e785c77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js @@ -2,7 +2,7 @@ //with a specified callback, this callback will receive an object with a selection on it function memberGroupPicker($scope, editorService, memberGroupResource, localizationService, overlayService){ - var vm = this; + const vm = this; vm.openMemberGroupPicker = openMemberGroupPicker; vm.remove = remove; @@ -13,12 +13,13 @@ function memberGroupPicker($scope, editorService, memberGroupResource, localizat return str.replace(rgxtrim, ''); } - var removeAllEntriesAction = { - labelKey: 'clipboard_labelForRemoveAllEntries', + let removeAllEntriesAction = { + labelKey: "clipboard_labelForRemoveAllEntries", labelTokens: [], - icon: 'trash', + icon: "icon-trash", method: removeAllEntries, - isDisabled: true + isDisabled: true, + useLegacyIcon: false }; $scope.renderModel = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 962fd46549..ce90a288e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -1,4 +1,4 @@ -function multiUrlPickerController($scope, localizationService, entityResource, iconHelper, editorService) { +function multiUrlPickerController($scope, localizationService, entityResource, iconHelper, editorService, overlayService) { var vm = { labels: { @@ -6,12 +6,29 @@ function multiUrlPickerController($scope, localizationService, entityResource, i } }; + let removeAllEntriesAction = { + labelKey: "clipboard_labelForRemoveAllEntries", + labelTokens: [], + icon: "icon-trash", + method: removeAllEntries, + isDisabled: true, + useLegacyIcon: false + }; + $scope.renderModel = []; if ($scope.preview) { return; } + if ($scope.model.config && parseInt($scope.model.config.maxNumber) !== 1 && $scope.umbProperty) { + var propertyActions = [ + removeAllEntriesAction + ]; + + $scope.umbProperty.setPropertyActions(propertyActions); + } + if (!Array.isArray($scope.model.value)) { $scope.model.value = []; } @@ -58,6 +75,9 @@ function multiUrlPickerController($scope, localizationService, entityResource, i $scope.multiUrlPickerForm.maxCount.$setValidity("maxCount", true); } $scope.sortableOptions.disabled = $scope.renderModel.length === 1; + + removeAllEntriesAction.isDisabled = $scope.renderModel.length === 0; + //Update value $scope.model.value = $scope.renderModel; } @@ -65,10 +85,16 @@ function multiUrlPickerController($scope, localizationService, entityResource, i $scope.remove = function ($index) { $scope.renderModel.splice($index, 1); - + setDirty(); }; + $scope.clear = function ($index) { + $scope.renderModel = []; + + setDirty(); + }; + $scope.openLinkPicker = function (link, $index) { var target = link ? { name: link.name, @@ -143,6 +169,22 @@ function multiUrlPickerController($scope, localizationService, entityResource, i } } + function removeAllEntries() { + localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function () { + overlayService.close(); + }, + submit: function () { + $scope.clear(); + overlayService.close(); + } + }); + }); + } + function init() { localizationService.localizeMany(["general_recycleBin"]) 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 0c4da2c4ab..d406689116 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,4 +1,4 @@ -(function () { +(function () { 'use strict'; /** @@ -67,7 +67,8 @@ function NestedContentController($scope, $interpolate, $filter, serverValidationManager, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { - var vm = this; + const vm = this; + var model = $scope.$parent.$parent.model; var contentTypeAliases = []; @@ -135,17 +136,27 @@ }); } - var copyAllEntriesAction = { - labelKey: 'clipboard_labelForCopyAllEntries', + let copyAllEntriesAction = { + labelKey: "clipboard_labelForCopyAllEntries", labelTokens: [model.label], - icon: 'documents', + icon: "icon-documents", method: copyAllEntries, - isDisabled: true - } + isDisabled: true, + useLegacyIcon: false + }; + let removeAllEntriesAction = { + labelKey: "clipboard_labelForRemoveAllEntries", + labelTokens: [], + icon: "icon-trash", + method: removeAllEntries, + isDisabled: true, + useLegacyIcon: false + }; + + function removeAllEntries() { - var removeAllEntries = function () { - localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(function (data) { + localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(data => { overlayService.confirmDelete({ title: data[1], content: data[0], @@ -161,22 +172,12 @@ }); }); } - - var removeAllEntriesAction = { - labelKey: 'clipboard_labelForRemoveAllEntries', - labelTokens: [], - icon: 'trash', - method: removeAllEntries, - isDisabled: true - }; - + // helper to force the current form into the dirty state function setDirty() { - if (vm.umbProperty) { vm.umbProperty.setDirty(); } - }; function addNode(alias) { @@ -729,8 +730,6 @@ removeAllEntriesAction.isDisabled = copyAllEntriesAction.isDisabled; } - - var propertyActions = [ copyAllEntriesAction, removeAllEntriesAction From 77dce77df447a8d27d85d7b986cd1e5c02a1cee9 Mon Sep 17 00:00:00 2001 From: Erik-Jan Westendorp Date: Mon, 23 May 2022 08:58:39 +0200 Subject: [PATCH 019/101] Add Two Factor Auth translation (Dutch) --- src/Umbraco.Core/EmbeddedResources/Lang/nl.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 8f94e8694d..e027a2c9ab 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -996,6 +996,14 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Umbraco: Wachtwoord Reset De gebruikersnaam om in te loggen bij jouw Umbraco omgeving is: %0%

Klik hier om je wachtwoord te resetten of knip/plak deze URL in je browser:

%1%

]]>
+ Umbraco: Beveiligingscode + Jouw beveiligingscode is: %0% + Laatste stap + Je hebt tweestapsverificatie ingeschakeld en moet je identiteit verifiëren. + Kies een tweestapsverificatie aanbieder + Verificatiecode + Vul de verificatiecode in + Ongeldige code ingevoerd Dashboard From 71156fb59bc222febccf63c1b6d0278471a698ad Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Tue, 24 May 2022 00:04:33 +0200 Subject: [PATCH 020/101] Adjust Danish translation for 2FA (#12459) * Adjust Danish translation for 2FA * Remove break --- src/Umbraco.Core/EmbeddedResources/Lang/da.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 8717ba3bb0..256a1635a0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -1028,12 +1028,14 @@ Umbraco: Nulstil adgangskode Dit brugernavn til at logge på Umbraco backoffice er: %0%

Klik her for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:

%1%

]]>
+ Umbraco: Sikkerhedskode + Din sikkerhedskode er: %0% Sidste skridt - Det er påkrævet at du verificerer din identitet. - Vælg venligst en autentificeringsmetode + Det er påkrævet at du bekræfter din identitet. + Vælg venligst en godkendelsesmetode Kode - Indtast venligst koden fra dit device - Koden kunne ikke genkendes + Indtast venligst bekræftelseskoden + Ugyldig kode indtastet Skrivebord From 3b6870333343c6c423380a25e2b71233cfab6f44 Mon Sep 17 00:00:00 2001 From: Erik-Jan Westendorp Date: Mon, 23 May 2022 12:32:02 +0200 Subject: [PATCH 021/101] Translate 'Configure Two Factor' (Dutch) --- src/Umbraco.Core/EmbeddedResources/Lang/nl.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index e027a2c9ab..65ebbac15a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -1942,6 +1942,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Wijzig je foto zodat andere gebruikers je makkelijk kunnen herkennen. Auteur + Configureer tweestapsverificatie Wijzig Je profiel Je recente historie From d6b60cfc88c956c13959e99229eaf06fb1ff6ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asbj=C3=B8rn=20Riis-Knudsen?= Date: Tue, 24 May 2022 17:41:10 +0200 Subject: [PATCH 022/101] Fix #12454 by having Coalesce handle null values (#12456) * Fix #12454 by having Coalesce handle null values * Allow null values in Html.Coalesce #12454 --- src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs | 4 ++-- .../Extensions/HtmlHelperRenderExtensions.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs b/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs index c043161486..90c4307807 100644 --- a/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs +++ b/src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs @@ -89,10 +89,10 @@ public sealed class HtmlStringUtilities return sb.ToString(); } - public string Coalesce(params object[] args) + public string Coalesce(params object?[] args) { var arg = args - .Select(x => x.ToString()) + .Select(x => x?.ToString()) .FirstOrDefault(x => string.IsNullOrWhiteSpace(x) == false); return arg ?? string.Empty; diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index 0f30a1dcd5..13a606b28b 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -910,7 +910,7 @@ public static class HtmlHelperRenderExtensions /// /// Will take the first non-null value in the collection and return the value of it. /// - public static string Coalesce(this IHtmlHelper helper, params object[] args) + public static string Coalesce(this IHtmlHelper helper, params object?[] args) => s_stringUtilities.Coalesce(args); /// From 019385411ad0e9fba0ca3d76e7dbf74b69e9b9c5 Mon Sep 17 00:00:00 2001 From: Wilmar de Hoogd Date: Wed, 25 May 2022 02:31:52 +0200 Subject: [PATCH 023/101] added option to disable automatic expand of single nc item (#12261) * added option to disable automatic expand of single nc item * remove max item conditional remove max item 1 conditional to set current node * changed description of slider property removed description of first item conditional * Update NestedContentConfiguration.cs change copy to more accurately describe the feature Co-authored-by: Wilmar --- .../PropertyEditors/NestedContentConfiguration.cs | 3 +++ .../nestedcontent/nestedcontent.controller.js | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs b/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs index aed6b5cd00..5cdfd4ba15 100644 --- a/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/NestedContentConfiguration.cs @@ -23,6 +23,9 @@ namespace Umbraco.Cms.Core.PropertyEditors [ConfigurationField("showIcons", "Show Icons", "boolean", Description = "Show the Element Type icons.")] public bool ShowIcons { get; set; } = true; + [ConfigurationField("expandsOnLoad", "Expands on load", "boolean", Description = "A single item is automatically expanded")] + public bool ExpandsOnLoad { get; set; } = true; + [ConfigurationField("hideLabel", "Hide Label", "boolean", Description = "Hide the property label and let the item list span the full width of the editor window.")] public bool HideLabel { get; set; } 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 d406689116..b678492a0b 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 @@ -94,7 +94,8 @@ if (vm.maxItems === 0) vm.maxItems = 1000; - vm.singleMode = vm.minItems === 1 && vm.maxItems === 1 && model.config.contentTypes.length === 1;; + vm.singleMode = vm.minItems === 1 && vm.maxItems === 1 && model.config.contentTypes.length === 1; + vm.expandsOnLoad = Object.toBoolean(model.config.expandsOnLoad) vm.showIcons = Object.toBoolean(model.config.showIcons); vm.wideMode = Object.toBoolean(model.config.hideLabel); vm.hasContentTypes = model.config.contentTypes.length > 0; @@ -617,8 +618,8 @@ modelWasChanged = true; } - // If there is only one item, set it as current node - if (vm.singleMode || (vm.nodes.length === 1 && vm.maxItems === 1)) { + // If there is only one item and expandsOnLoad property is true, set it as current node + if (vm.singleMode || (vm.expandsOnLoad && vm.nodes.length === 1)) { setCurrentNode(vm.nodes[0], false); } From 83e555a8ede703e91ac9e3e9e57f00820397b69d Mon Sep 17 00:00:00 2001 From: Johan Runsten Date: Wed, 25 May 2022 10:13:25 +0200 Subject: [PATCH 024/101] Fixed null check typo in CacheInstructionService. Fixes #12473. (#12474) * Fixed null check typo. Fixes #12473. * Removed unneccessary null forgiving operator Co-authored-by: Johan Runsten --- .../Services/CacheInstructionService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs index 12fd97acab..b9cfce96e9 100644 --- a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs @@ -314,12 +314,12 @@ namespace Umbraco.Cms private static List GetAllInstructions(IEnumerable? jsonInstructions) { var result = new List(); - if (jsonInstructions is not null) + if (jsonInstructions is null) { return result; } - foreach (JToken jsonItem in jsonInstructions!) + foreach (JToken jsonItem in jsonInstructions) { // Could be a JObject in which case we can convert to a RefreshInstruction. // Otherwise it could be another JArray - in which case we'll iterate that. From 8609314314c53db90b8f71237bd335d440981a40 Mon Sep 17 00:00:00 2001 From: Jeavon Date: Thu, 26 May 2022 06:39:21 +0100 Subject: [PATCH 025/101] Update template gitignore for static assets now in Razor Class Library (#12424) * Update template gitignore Remove the rules for umbraco static assets as these are now served from a Razor Class Libray * Update .gitignore Adjust path of appsettings-schema.json --- templates/UmbracoProject/.gitignore | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/templates/UmbracoProject/.gitignore b/templates/UmbracoProject/.gitignore index 602728d104..59f9d603e0 100644 --- a/templates/UmbracoProject/.gitignore +++ b/templates/UmbracoProject/.gitignore @@ -464,24 +464,6 @@ $RECYCLE.BIN/ # Umbraco log files **/umbraco/Logs/ -# Umbraco backoffice language files -# Nuget package Umbraco.Cms.StaticAssets will copy them in during dotnet build -# Customize langguage files in /config/lang/{language}.user.xml -**/umbraco/config/lang/ - # JSON Schema file for appsettings # This is auto generated from the build -**/umbraco/config/appsettings-schema.json - -# This is the no-nodes, installer & upgrader pages from Umbraco -# Nuget package Umbraco.Cms.StaticAssets will copy them in during dotnet build -**/umbraco/UmbracoWebsite/ -**/umbraco/UmbracoInstall/ -**/umbraco/UmbracoBackOffice/ - -# Comment out the line below if you wish to change or add any new templates to PartialView Macros -**/umbraco/PartialViewMacros/ - -# Umbraco Static Assets of Backoffice -# Nuget package Umbraco.Cms.StaticAssets will copy them in during dotnet build -**/wwwroot/umbraco/ +appsettings-schema.json From f3c4c677f3cc67d7d505a7d1c227ad6d9b32e71c Mon Sep 17 00:00:00 2001 From: Matt Brailsford Date: Thu, 26 May 2022 08:25:02 +0100 Subject: [PATCH 026/101] Don't use default params for AddUnique extension methods (breaks v9) (#12485) --- .../ServiceCollectionExtensions.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs index 6c806ce0db..d0f198557f 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -37,6 +37,19 @@ namespace Umbraco.Extensions services.Add(ServiceDescriptor.Describe(typeof(TService), typeof(TImplementing), lifetime)); } + /// + /// Adds services of types & with a shared implementation type of to the specified . + /// + /// + /// Removes all previous registrations for the types & . + /// + public static void AddMultipleUnique( + this IServiceCollection services) + where TService1 : class + where TService2 : class + where TImplementing : class, TService1, TService2 + => services.AddMultipleUnique(ServiceLifetime.Singleton); + /// /// Adds services of types & with a shared implementation type of to the specified . /// @@ -45,7 +58,7 @@ namespace Umbraco.Extensions /// public static void AddMultipleUnique( this IServiceCollection services, - ServiceLifetime lifetime = ServiceLifetime.Singleton) + ServiceLifetime lifetime) where TService1 : class where TService2 : class where TImplementing : class, TService1, TService2 @@ -63,6 +76,18 @@ namespace Umbraco.Extensions services.AddSingleton(); } + /// + /// Adds a service of type with an implementation factory method to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + Func factory) + where TService : class + => services.AddUnique(factory, ServiceLifetime.Singleton); + /// /// Adds a service of type with an implementation factory method to the specified . /// @@ -72,7 +97,7 @@ namespace Umbraco.Extensions public static void AddUnique( this IServiceCollection services, Func factory, - ServiceLifetime lifetime = ServiceLifetime.Singleton) + ServiceLifetime lifetime) where TService : class { services.RemoveAll(); From d0a752248ec92ca0703a0439d1260cd81ba9dc2a Mon Sep 17 00:00:00 2001 From: Matt Brailsford Date: Thu, 26 May 2022 08:54:05 +0100 Subject: [PATCH 027/101] Make ActionDelete.ActionAlias public again --- src/Umbraco.Core/Actions/ActionDelete.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Actions/ActionDelete.cs b/src/Umbraco.Core/Actions/ActionDelete.cs index 88e79d472a..44c33cc102 100644 --- a/src/Umbraco.Core/Actions/ActionDelete.cs +++ b/src/Umbraco.Core/Actions/ActionDelete.cs @@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Actions /// /// The unique action alias /// - private const string ActionAlias = "delete"; + public const string ActionAlias = "delete"; /// /// The unique action letter From 4238e2a4f3a418c8d8cb68f8efdc59b83ce768d8 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 2 Jun 2022 15:47:47 +0200 Subject: [PATCH 028/101] Simplify code in UmbracoViewPage (#12528) --- src/Umbraco.Web.Common/Views/UmbracoViewPage.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index b1ac11c77d..9ac8d917f5 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -39,21 +39,19 @@ public abstract class UmbracoViewPage : RazorPage return _helper; } + _helper = Context.RequestServices.GetRequiredService(); + TModel model = ViewData.Model; var content = model as IPublishedContent; + if (content is null && model is IContentModel contentModel) { content = contentModel.Content; } - if (content is null) - { - content = UmbracoContext?.PublishedRequest?.PublishedContent; - } + content ??= UmbracoContext?.PublishedRequest?.PublishedContent; - _helper = Context.RequestServices.GetRequiredService(); - - if (!(content is null)) + if (content is not null) { _helper.AssignedContentItem = content; } From 23e94ffdad72e87a8ce797b1035e06d1a9cc4246 Mon Sep 17 00:00:00 2001 From: Matt Brailsford Date: Fri, 17 Jun 2022 09:23:43 +0200 Subject: [PATCH 029/101] Add TOC to Contributing page (#12587) --- .github/CONTRIBUTING.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0f5d8a7752..28618fb548 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -14,8 +14,22 @@ We also encourage community members to feel free to comment on others' pull requ ## Table of contents -↖️ You can jump to any section by using the "table of contents" button ( ![Table of contents icon](img/tableofcontentsicon.svg) ) above. - +- [Before you start](#before-you-start) + * [Code of Conduct](#code-of-conduct) + * [What can I contribute?](#what-can-i-contribute) + + [Making larger changes](#making-larger-changes) + + [Pull request or package?](#pull-request-or-package) + + [Ownership and copyright](#ownership-and-copyright) +- [Finding your first issue: Up for grabs](#finding-your-first-issue-up-for-grabs) +- [Making your changes](#making-your-changes) + + [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) + + [Style guide](#style-guide) + + [Questions?](#questions) +- [Creating a pull request](#creating-a-pull-request) +- [The review process](#the-review-process) + * [Dealing with requested changes](#dealing-with-requested-changes) + + [No longer available?](#no-longer-available) + * [The Core Collaborators team](#the-core-collaborators-team) ## Before you start @@ -230,4 +244,4 @@ These wonderful people aim to provide you with a reply to your PR, review and te [up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs [Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS [issues]: https://github.com/umbraco/Umbraco-CMS/issues -[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions \ No newline at end of file +[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions From a9b5558eb1be6ca0ed1611c0eee0a2237073ecd9 Mon Sep 17 00:00:00 2001 From: Mehrdad Aghababaei Babaki Date: Mon, 6 Jun 2022 23:23:55 +1000 Subject: [PATCH 030/101] remove lightbox from dom on destroy --- .../src/common/directives/components/umblightbox.directive.js | 2 ++ 1 file changed, 2 insertions(+) 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 8d223e427e..0874c54cd2 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 @@ -94,6 +94,8 @@ for (var e in eventBindings) { eventBindings[e](); } + + el.remove(); }); } From 2f4feb925a66781529eeea779c409b3d73ea7fdd Mon Sep 17 00:00:00 2001 From: DanielToxic <97676759+DanielToxic@users.noreply.github.com> Date: Fri, 27 May 2022 12:00:29 +0200 Subject: [PATCH 031/101] Localization: Add missing 'Create a new Document Type' translation (Swedish) Add 'Create a new Document Type' key in button area (Swedish translation file) --- src/Umbraco.Core/EmbeddedResources/Lang/sv.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml index ad6f18b4cd..af3f157bf4 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/sv.xml @@ -123,6 +123,7 @@ Punktlista Numrerad lista Infoga macro + Skapa en ny dokumenttyp Infoga bild Publicera och stäng Ändra relation From d42a695e8a4857ec99e20138dea8b1cae5b58c0d Mon Sep 17 00:00:00 2001 From: Johannes Lantz Date: Fri, 17 Jun 2022 10:21:01 +0200 Subject: [PATCH 032/101] Added dictionary import/export (#12378) * Added dictionary import/export * Added umb tree to dictionary import & level for displaying preview dictionary import * Indented dictionaries for import, added new text for choosing where to import dictionary items * Removed console.log for dictionary/import.controller.js Co-authored-by: Michael --- .../EmbeddedResources/Lang/en.xml | 9 ++ .../EmbeddedResources/Lang/en_us.xml | 9 ++ .../Models/DictionaryImportModel.cs | 14 ++ .../Models/DictionaryPreviewImportModel.cs | 14 ++ .../Packaging/PackageDataInstallation.cs | 6 + .../Controllers/DictionaryController.cs | 136 +++++++++++++++++- .../Trees/DictionaryTreeController.cs | 19 ++- .../common/resources/dictionary.resource.js | 73 ++++++++++ .../src/views/dictionary/export.controller.js | 18 +++ .../src/views/dictionary/export.html | 17 +++ .../src/views/dictionary/import.controller.js | 67 +++++++++ .../src/views/dictionary/import.html | 81 +++++++++++ 12 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Models/DictionaryImportModel.cs create mode 100644 src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs create mode 100644 src/Umbraco.Web.UI.Client/src/views/dictionary/export.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/dictionary/export.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/dictionary/import.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/dictionary/import.html diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 1b7bcbf2d0..6e24d62eac 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -36,6 +36,7 @@ Restore Choose where to copy Choose where to move + Choose where to import to in the tree structure below Choose where to copy the selected item(s) Choose where to move the selected item(s) @@ -569,9 +570,14 @@ Modifying layout will result in loss of data for any existing content that is based on this configuration. + + To import a dictionary item, find the ".udt" file on your computer by clicking the + "Import" button (you'll be asked for confirmation on the next screen) + Dictionary item does not exist. Parent item does not exist. There are no dictionary items. + There are no dictionary items in this file. Create dictionary item @@ -1592,6 +1598,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Invitation has been re-sent to %0% Document Type was exported to file An error occurred while exporting the Document Type + Dictionary item(s) was exported to file + An error occurred while exporting the dictionary item(s) + The following dictionary item(s) has been imported! Domains are not configured for multilingual site, please contact an administrator, see log for more information diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 7ebce617b5..db640d24ce 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -37,6 +37,7 @@ Restore Choose where to copy Choose where to move + Choose where to import to in the tree structure below Choose where to copy the selected item(s) Choose where to move the selected item(s) @@ -579,9 +580,14 @@ Modifying layout will result in loss of data for any existing content that is based on this configuration. + + To import a dictionary item, find the ".udt" file on your computer by clicking the + "Import" button (you'll be asked for confirmation on the next screen) + Dictionary item does not exist. Parent item does not exist. There are no dictionary items. + There are no dictionary items in this file. Create dictionary item @@ -1629,6 +1635,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Validation failed for language '%0%' Document Type was exported to file An error occurred while exporting the Document Type + Dictionary item(s) was exported to file + An error occurred while exporting the dictionary item(s) + The following dictionary item(s) has been imported! The release date cannot be in the past Cannot schedule the document for publishing since the required '%0%' is not published diff --git a/src/Umbraco.Core/Models/DictionaryImportModel.cs b/src/Umbraco.Core/Models/DictionaryImportModel.cs new file mode 100644 index 0000000000..2507a6a1ec --- /dev/null +++ b/src/Umbraco.Core/Models/DictionaryImportModel.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract(Name = "dictionaryImportModel")] + public class DictionaryImportModel + { + [DataMember(Name = "dictionaryItems")] + public List? DictionaryItems { get; set; } + + [DataMember(Name = "tempFileName")] + public string? TempFileName { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs b/src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs new file mode 100644 index 0000000000..530d49b013 --- /dev/null +++ b/src/Umbraco.Core/Models/DictionaryPreviewImportModel.cs @@ -0,0 +1,14 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract(Name = "dictionaryPreviewImportModel")] + public class DictionaryPreviewImportModel + { + [DataMember(Name = "name")] + public string? Name { get; set; } + + [DataMember(Name = "level")] + public int Level { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index f0bca5f1ea..9295b7d5ac 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1355,6 +1355,12 @@ namespace Umbraco.Cms.Infrastructure.Packaging return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId); } + public IEnumerable ImportDictionaryItem(XElement dictionaryItemElement, int userId, Guid? parentId) + { + var languages = _localizationService.GetAllLanguages().ToList(); + return ImportDictionaryItem(dictionaryItemElement, languages, parentId, userId); + } + private IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, List languages, Guid? parentId, int userId) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 91e3385242..14c190388f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -1,4 +1,5 @@ using System; +using System.Xml; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -14,12 +15,16 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; +using Umbraco.Cms.Infrastructure.Packaging; using Constants = Umbraco.Cms.Core.Constants; +using System.Xml.Linq; +using Microsoft.AspNetCore.Http; namespace Umbraco.Cms.Web.BackOffice.Controllers { @@ -42,6 +47,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly GlobalSettings _globalSettings; private readonly ILocalizedTextService _localizedTextService; private readonly IUmbracoMapper _umbracoMapper; + private readonly IEntityXmlSerializer _serializer; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly PackageDataInstallation _packageDataInstallation; public DictionaryController( ILogger logger, @@ -49,7 +57,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IBackOfficeSecurityAccessor backofficeSecurityAccessor, IOptionsSnapshot globalSettings, ILocalizedTextService localizedTextService, - IUmbracoMapper umbracoMapper + IUmbracoMapper umbracoMapper, + IEntityXmlSerializer serializer, + IHostingEnvironment hostingEnvironment, + PackageDataInstallation packageDataInstallation ) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -58,6 +69,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); } /// @@ -354,6 +368,126 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } + public IActionResult ExportDictionary(int id, bool includeChildren = false) + { + var dictionaryItem = _localizationService.GetDictionaryItemById(id); + if (dictionaryItem == null) + throw new NullReferenceException("No dictionary item found with id " + id); + + var xml = _serializer.Serialize(dictionaryItem, includeChildren); + + var fileName = $"{dictionaryItem.ItemKey}.udt"; + // Set custom header so umbRequestHelper.downloadFile can save the correct filename + HttpContext.Response.Headers.Add("x-filename", fileName); + + return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName); + } + + public IActionResult ImportDictionary(string file, int parentId) + { + if (string.IsNullOrWhiteSpace(file)) + return NotFound(); + + var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); + if (!System.IO.File.Exists(filePath)) + return NotFound(); + + var xd = new XmlDocument { XmlResolver = null }; + xd.Load(filePath); + + var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0; + var element = XElement.Parse(xd.InnerXml); + + var parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId); + var dictionaryItems = _packageDataInstallation.ImportDictionaryItem(element, userId, parentDictionaryItem?.Key); + + // Try to clean up the temporary file. + try + { + System.IO.File.Delete(filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath); + } + + var model = _umbracoMapper.Map(dictionaryItems.FirstOrDefault()); + return Content(model!.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); + } + + public ActionResult Upload(IFormFile file) + { + + if (file == null) + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); + + var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); + var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); + var tempPath = Path.Combine(root, fileName); + + if (!Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root))) + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("media", "invalidFileName")); + + if (!ext.InvariantEquals("udt")) + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("media", "disallowedFileType")); + + using (var stream = System.IO.File.Create(tempPath)) + { + file.CopyToAsync(stream).GetAwaiter().GetResult(); + } + + var xd = new XmlDocument + { + XmlResolver = null + }; + xd.Load(tempPath); + + if (xd.DocumentElement == null) + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); + + DictionaryImportModel model = new DictionaryImportModel() + { + TempFileName = tempPath, + DictionaryItems = new List() + }; + + int level = 1; + string curentParrent = string.Empty; + foreach (XmlNode dictionaryItem in xd.GetElementsByTagName("DictionaryItem")) + { + var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty; + var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; + + if (parentKey != curentParrent || level == 1) + { + level += 1; + curentParrent = parentKey; + } + + model.DictionaryItems.Add(new DictionaryPreviewImportModel() + { + Level = level, + Name = name + }); + } + + if (!model.DictionaryItems.Any()) + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("dictionary", "noItemsInFile")); + + return model; + } + private static Func ItemSort() => item => item.ItemKey; } } diff --git a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs index d559b87d87..0795c9b282 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -130,8 +130,25 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (id != Constants.System.RootString) { - menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(new MenuItem("export", LocalizedTextService) + { + Icon = "icon-download-alt", + SeparatorBefore = true, + OpensDialog = true, + UseLegacyIcon = false + }); + menu.Items.Add(LocalizedTextService, true, opensDialog: true, useLegacyIcon: false); + } + else + { + menu.Items.Add(new MenuItem("import", LocalizedTextService) + { + Icon = "icon-page-up", + SeparatorBefore = true, + OpensDialog = true, + UseLegacyIcon = false + }); } menu.Items.Add(new RefreshNode(LocalizedTextService, true)); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js index 38a96fbcda..fd81668d04 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js @@ -160,6 +160,77 @@ function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataForma "Failed to save data for dictionary id " + dictionary.id); } + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#export + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Export dictionary items of a given id. + * + * ##usage + *
+        * dictionaryResource.exportItem(1234){
+        *    .then(function() {
+        *       Do stuff..
+        *    });
+        * 
+ * + * @param {Int} id the ID of the dictionary item so export + * @param {Bool?} includeChildren if children should also be exported + * @returns {Promise} resourcePromise object. + * + */ + function exportItem(id, includeChildren) { + if (!id) { + throw "id cannot be null"; + } + + var url = umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "ExportDictionary", { id: id, includeChildren: includeChildren }); + + return umbRequestHelper.downloadFile(url).then(function () { + localizationService.localize("speechBubbles_dictionaryItemExportedSuccess").then(function(value) { + notificationsService.success(value); + }); + }, function (data) { + localizationService.localize("speechBubbles_dictionaryItemExportedError").then(function(value) { + notificationsService.error(value); + }); + }); + } + + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#import + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Import a dictionary item from a file + * + * ##usage + *
+        * dictionaryResource.importItem("path to file"){
+        *    .then(function() {
+        *       Do stuff..
+        *    });
+        * 
+ * + * @param {String} file path of the file to import + * @param {Int?} parentId the int of the parent dictionary item to move incomming dictionary items to + * @returns {Promise} resourcePromise object. + * + */ + function importItem(file, parentId) { + if (!file) { + throw "file cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "ImportDictionary", { file: file, parentId: parentId })), + "Failed to import dictionary item " + file + ); + } + /** * @ngdoc method * @name umbraco.resources.dictionaryResource#getList @@ -194,6 +265,8 @@ function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataForma getById: getById, save: save, move: move, + exportItem: exportItem, + importItem: importItem, getList : getList }; diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/export.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/export.controller.js new file mode 100644 index 0000000000..df922e6d96 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/export.controller.js @@ -0,0 +1,18 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.Dictionary.ExportController", + function ($scope, dictionaryResource, navigationService) { + $scope.includeChildren = false; + + $scope.toggleHandler = function () { + $scope.includeChildren = !$scope.includeChildren + }; + + $scope.export = function () { + dictionaryResource.exportItem($scope.currentNode.id, $scope.includeChildren); + navigationService.hideMenu(); + }; + + $scope.cancel = function () { + navigationService.hideDialog(); + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/export.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/export.html new file mode 100644 index 0000000000..c1f5c6b2da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/export.html @@ -0,0 +1,17 @@ +
+
+ + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/import.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/import.controller.js new file mode 100644 index 0000000000..3936217343 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/import.controller.js @@ -0,0 +1,67 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.Dictionary.ImportController", + function ($scope, dictionaryResource, notificationsService, navigationService, Upload, umbRequestHelper) { + var vm = this; + + vm.state = "upload"; + vm.model = {}; + vm.uploadStatus = ""; + + vm.cancelButtonLabel = "cancel"; + + $scope.dialogTreeApi = {}; + + function nodeSelectHandler(args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.handleFiles = function (files, event, invalidFiles) { + if (files && files.length > 0) { + $scope.upload(files[0]); + } + }; + + $scope.upload = function (file) { + Upload.upload({ + url: umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "Upload"), + fields: {}, + file: file + }).then(function (response) { + + vm.model = response.data; + vm.state = "confirm"; + vm.uploadStatus = "done"; + + }, function (err) { + notificationsService.error(err.data.notifications[0].header, err.data.notifications[0].message); + }); + }; + + $scope.import = function () { + var parentId = $scope.target !== undefined ? $scope.target.id : 0; + + dictionaryResource.importItem(vm.model.tempFileName, parentId).then(function (path) { + navigationService.syncTree({ tree: "dictionary", path: path, forceReload: true, activate: false }); + + vm.state = "done"; + vm.cancelButtonLabel = "general_close"; + }); + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + }; + + $scope.close = function () { + navigationService.hideDialog(); + }; + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/import.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/import.html new file mode 100644 index 0000000000..64f810f4dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/import.html @@ -0,0 +1,81 @@ +
+ + + + +
From 4a6a318f2c00610c0588c00bad4e9f6105f3e47d Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Jun 2022 14:53:56 +0200 Subject: [PATCH 033/101] Add create content with same name test (#12557) --- .../cypress/integration/Content/content.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts index 0e12541338..9288dd6f72 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/content.ts @@ -792,4 +792,45 @@ context('Content', () => { cy.umbracoEnsureMacroNameNotExists(macroName); cy.umbracoEnsurePartialViewMacroFileNameNotExists(macroFileName); }); + + it('Create content with the same name', () => { + const documentTypeName = "TestType"; + const nodeName = "Home"; + const expectedNodeName = nodeName + " (1)"; + + cy.umbracoEnsureDocumentTypeNameNotExists(documentTypeName); + cy.deleteAllContent(); + + const documentType = new DocumentTypeBuilder() + .withName(documentTypeName) + .withAllowAsRoot(true) + .build(); + + cy.saveDocumentType(documentType).then((generatedDocumentType) => { + const rootContentNode = new ContentBuilder() + .withAction("saveNew") + .withContentTypeAlias(generatedDocumentType["alias"]) + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + + cy.saveContent(rootContentNode) + }); + + refreshContentTree(); + + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.umb-action-link').contains(documentTypeName).click(); + cy.umbracoEditorHeaderName(nodeName); + cy.umbracoButtonByLabelKey("buttons_saveAndPublish").click(); + + cy.umbracoSuccessNotification().should('be.visible'); + cy.umbracoTreeItem("content", [expectedNodeName]).should('be.visible'); + + cy.umbracoEnsureDocumentTypeNameNotExists(documentTypeName); + cy.deleteAllContent(); + }); }); From c840ceecffabcd8bc7f5971b7ad3a9d8a074d14a Mon Sep 17 00:00:00 2001 From: Matthew Care Date: Mon, 27 Jun 2022 01:03:07 +0200 Subject: [PATCH 034/101] Allow action links to wrap (#12611) * Allow action links to wrap Preventing wrapping means that buttons can overflow their container. * slightly wider context menu to reduce runty labels Co-authored-by: Nathan Woulfe --- src/Umbraco.Web.UI.Client/src/less/application/grid.less | 2 +- .../src/less/components/tree/umb-actions.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index 68160923c8..95511a232f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -133,7 +133,7 @@ body.umb-drawer-is-visible #mainwrapper{ position: absolute; top: 0px; left: 100%; - min-width: 250px; + min-width: 260px; } #speechbubble { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less index 0c231830de..887f5d1838 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -28,7 +28,6 @@ .umb-action-link { position: relative; - white-space: nowrap; font-size: 15px; color: @black; padding: 9px 25px 9px 20px; @@ -37,6 +36,7 @@ display: flex; width: 100%; align-items: center; + text-align: left; body.touch & { padding: 7px 25px 7px 20px; From 091cb9e428d003db794e668b20a23eaf774a75c3 Mon Sep 17 00:00:00 2001 From: Mehrdad Babaki Date: Mon, 27 Jun 2022 09:18:52 +1000 Subject: [PATCH 035/101] add a gap between icon and label of media menu (#12571) * add a gap between icon and label of media menu * replace inline style with class * icon alignment Co-authored-by: Nathan Woulfe --- src/Umbraco.Web.UI.Client/src/less/navs.less | 1 + .../src/views/propertyeditors/listview/listview.html | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index db18d3f9d2..6b4a6abb30 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -272,6 +272,7 @@ color: @ui-option-type; display: flex; justify-content: flex-start; + align-items: center; clear: both; font-weight: normal; line-height: 20px; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 7fad01fe6c..602e42425b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -50,16 +50,15 @@ - + - From b60c2ca5d3d25e26f18e0948283182742118b35f Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 2 Jun 2022 13:38:40 +0200 Subject: [PATCH 036/101] Update aria-invalid based on valid length --- .../src/views/propertyeditors/textarea/textarea.html | 3 ++- .../src/views/propertyeditors/textbox/textbox.html | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 2f183c29f0..b4c96e9292 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 @@ -9,7 +9,8 @@ ng-keyup="change()" ng-trim="false" ng-required="model.validation.mandatory" - aria-required="{{model.validation.mandatory}}"> + aria-required="{{model.validation.mandatory}}" + aria-invalid="{{validLength ? false : true}}"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 1f1131c43f..a0d08d20ed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -8,7 +8,7 @@ val-server="value" ng-required="model.validation.mandatory" aria-required="{{model.validation.mandatory}}" - aria-invalid="False" + aria-invalid="{{validLength ? false : true}}" ng-trim="false" ng-change="change()" /> From ea264c79efda75ad4c05b15503964f9fd7e63608 Mon Sep 17 00:00:00 2001 From: Erik-Jan Westendorp Date: Mon, 27 Jun 2022 07:28:26 +0200 Subject: [PATCH 037/101] Translate 'Referenced by the following items' --- src/Umbraco.Core/EmbeddedResources/Lang/nl.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 65ebbac15a..a9513c5302 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -2296,6 +2296,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Gebruikt in Mediatypes Gebruikt in Ledentypes Gebruikt door + Heeft verwijzingen vanuit de volgende items Gebruikt in Documenten Gebruikt in Leden Gebruikt in Media From ce9f92909b3fe8b127afab48dd7970dcc859b716 Mon Sep 17 00:00:00 2001 From: Erik-Jan Westendorp Date: Wed, 29 Jun 2022 08:41:00 +0200 Subject: [PATCH 038/101] Remove Obsolete AddOEmbedProvider extension method (#12624) * Remove unused using * Remove Obsolete AddOEmbedProvider Method * Revert and update obsolete message --- .../DependencyInjection/UmbracoBuilder.CollectionBuilders.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs index cb40974375..2e88506720 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs @@ -85,7 +85,7 @@ namespace Umbraco.Cms.Core.DependencyInjection return builder; } - [Obsolete("Use AddEmbedProvider instead. This will be removed in Umbraco 10")] + [Obsolete("Use AddEmbedProvider instead. This will be removed in Umbraco 11")] public static IUmbracoBuilder AddOEmbedProvider(this IUmbracoBuilder builder) where T : class, IEmbedProvider => AddEmbedProvider(builder); From ccf0d3f443c20f544baebab1778e86b75900ef98 Mon Sep 17 00:00:00 2001 From: gilbertaoe Date: Wed, 29 Jun 2022 16:39:55 -0500 Subject: [PATCH 039/101] Update MemberRepository.cs Ensuring that MemberManager.ConfirmEmailAsync persists. --- .../Persistence/Repositories/Implement/MemberRepository.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs index 9c41482436..59e8cceac0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberRepository.cs @@ -790,6 +790,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement changedCols.Add("passwordConfig"); } + if (entity.IsPropertyDirty("EmailConfirmedDate")) + { + changedCols.Add("emailConfirmedDate"); + } + // If userlogin or the email has changed then need to reset security stamp if (changedCols.Contains("Email") || changedCols.Contains("LoginName")) { From 448836ee2df5f1e35d94515c7f6d544801b7fe2a Mon Sep 17 00:00:00 2001 From: Matthew Care Date: Wed, 22 Jun 2022 21:26:06 +0200 Subject: [PATCH 040/101] Break word for limited width content Add break word to avoid overflowing content / account for label values which are often used to store random data --- .../src/less/components/umb-node-preview.less | 1 - .../src/less/components/umb-readonlyvalue.less | 5 +++-- src/Umbraco.Web.UI.Client/src/less/mixins.less | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index ac7277109a..8a2b86de85 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -38,7 +38,6 @@ .umb-node-preview__content { flex: 1 1 auto; - margin-right: 25px; overflow: hidden; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less index 0790bdd07a..b2eca6613d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-readonlyvalue.less @@ -1,3 +1,4 @@ -.umb-readonlyvalue { - position:relative; +.umb-readonlyvalue { + position: relative; + .umb-property-editor--limit-width(); } diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index c0ddcd6cdb..78ccbe0ace 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -421,6 +421,7 @@ // Limit width of specific property editors .umb-property-editor--limit-width { max-width: @propertyEditorLimitedWidth; + word-break: break-all; } // Horizontal dividers From 5bb81558f8ff94ee8ab8e605147ab251e6f2a283 Mon Sep 17 00:00:00 2001 From: Dennis Date: Mon, 4 Jul 2022 01:26:19 +0200 Subject: [PATCH 041/101] set isInfoTab to true if active tab is info tab (#12632) * set isInfoTab to true if active tab is info tab * replace underscore functions with good old boring javascript Co-authored-by: Nathan Woulfe --- .../content/umbcontentnodeinfo.directive.js | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 501ea9f81a..aba292523d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -1,4 +1,4 @@ -(function () { +(function () { 'use strict'; function ContentNodeInfoDirective($timeout, logResource, eventsService, userService, localizationService, dateHelper, editorService, redirectUrlsResource, overlayService, entityResource) { @@ -9,6 +9,7 @@ var isInfoTab = false; var auditTrailLoaded = false; var labels = {}; + scope.publishStatus = []; scope.currentVariant = null; scope.currentUrls = []; @@ -22,19 +23,17 @@ scope.historyLabelKey = scope.node.variants && scope.node.variants.length === 1 ? "general_history" : "auditTrails_historyIncludingVariants"; function onInit() { - entityResource.getAll("Template").then(function (templates) { - scope.allTemplates = templates; - }); + entityResource.getAll("Template").then(templates => scope.allTemplates = templates); // set currentVariant - scope.currentVariant = _.find(scope.node.variants, (v) => v.active); + scope.currentVariant = scope.node.variants.find(v => v.active); updateCurrentUrls(); // if there are any infinite editors open we are in infinite editing scope.isInfiniteMode = editorService.getNumberOfEditors() > 0 ? true : false; - userService.getCurrentUser().then(function (user) { + userService.getCurrentUser().then(user => { // only allow change of media type if user has access to the settings sections const hasAccessToSettings = user.allowedSections.indexOf("settings") !== -1 ? true : false; scope.allowChangeDocumentType = hasAccessToSettings; @@ -54,7 +53,7 @@ ]; localizationService.localizeMany(keys) - .then(function (data) { + .then(data => { [labels.deleted, labels.unpublished, labels.published, @@ -81,7 +80,7 @@ }); scope.auditTrailOptions = { - "id": scope.node.id + id: scope.node.id }; // make sure dates are formatted to the user's locale @@ -101,10 +100,11 @@ scope.previewOpenUrl = '#/settings/documentTypes/edit/' + scope.documentType.id; } - var activeApp = _.find(scope.node.apps, (a) => a.active); + var activeApp = scope.node.apps.find(a => a.active); if (activeApp.alias === "umbInfo") { loadRedirectUrls(); loadAuditTrail(); + isInfoTab = true; } // never show templates for element types (if they happen to have been created in the content tree) @@ -118,12 +118,8 @@ scope.openDocumentType = function (documentType) { - const variantIsDirty = _.some(scope.node.variants, function (variant) { - return variant.isDirty; - }); - // add confirmation dialog before opening the doc type editor - if (variantIsDirty) { + if (scope.node.variants.some(variant => variant.isDirty)) { const confirm = { title: labels.unsavedChanges, view: "default", @@ -149,7 +145,7 @@ function openDocTypeEditor(documentType) { const editor = { id: documentType.id, - submit: function (model) { + submit: function () { editorService.close(); }, close: function () { @@ -160,13 +156,14 @@ } scope.openTemplate = function () { - var template = _.findWhere(scope.allTemplates, { alias: scope.node.template }) + var template = scope.allTemplates.find(x => x.alias === scope.node.template); + if (!template) { return; } var templateEditor = { id: template.id, - submit: function (model) { + submit: function () { editorService.close(); }, close: function () { @@ -185,7 +182,7 @@ var rollback = { node: scope.node, - submit: function (model) { + submit: function () { const args = { node: scope.node }; eventsService.emit("editors.content.reload", args); editorService.close(); @@ -211,7 +208,7 @@ // get current backoffice user and format dates userService.getCurrentUser().then(currentUser => { - Utilities.forEach(data.items, item => { + data.items.forEach(item => { item.timestampFormatted = dateHelper.getLocalDate(item.timestamp, currentUser.locale, 'LLL'); }); }); @@ -255,7 +252,7 @@ } function setAuditTrailLogTypeColor(auditTrail) { - Utilities.forEach(auditTrail, item => { + auditTrail.forEach(item => { switch (item.logType) { case "Save": @@ -325,10 +322,14 @@ // find the urls for the currently selected language // when there is no selected language (allow vary by culture == false), show all urls of the node. - scope.currentUrls = _.filter(scope.node.urls, (url) => (scope.currentVariant.language == null || scope.currentVariant.language.culture === url.culture)); + scope.currentUrls = scope.node.urls.filter(url => scope.currentVariant.language == null || scope.currentVariant.language.culture === url.culture); // figure out if multiple cultures apply across the content URLs - scope.currentUrlsHaveMultipleCultures = _.keys(_.groupBy(scope.currentUrls, url => url.culture)).length > 1; + // by getting an array of the url cultures, then checking that more than one culture exists in the array + scope.currentUrlsHaveMultipleCultures = scope.currentUrls + .map(x => x.culture) + .filter((v, i, arr) => arr.indexOf(v) === i) + .length > 1; } // load audit trail and redirects when on the info tab From 173c1c8c8f92b448f2398bb96233075f1d7c23e7 Mon Sep 17 00:00:00 2001 From: Erik-Jan Westendorp Date: Mon, 4 Jul 2022 01:54:27 +0200 Subject: [PATCH 042/101] Add Current Server Role to system information (#12630) * Add Current Server Rule to system information * Update Unit Tests, Mock IServerRoleAccessor * Add CurrentServerRole to UnitTest * adds trailing commas Co-authored-by: Nathan Woulfe --- src/Umbraco.Core/Constants-Telemetry.cs | 3 ++- .../SystemInformationTelemetryProvider.cs | 18 ++++++++++++------ .../Telemetry/TelemetryServiceTests.cs | 3 ++- .../Services/UserDataServiceTests.cs | 6 ++++-- .../SystemInformationTelemetryProviderTests.cs | 6 ++++-- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs index 6fc474d9ae..284841f104 100644 --- a/src/Umbraco.Core/Constants-Telemetry.cs +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { public static partial class Constants { @@ -27,6 +27,7 @@ public static string AspEnvironment = "AspEnvironment"; public static string IsDebug = "IsDebug"; public static string DatabaseProvider = "DatabaseProvider"; + public static string CurrentServerRole = "CurrentServerRole"; } } } diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs index 4d01a41cd9..d0d2a40eb3 100644 --- a/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs @@ -1,8 +1,5 @@ -using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; -using System.Threading; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -10,6 +7,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; using Umbraco.Extensions; @@ -25,6 +23,8 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, private readonly ModelsBuilderSettings _modelsBuilderSettings; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; private readonly IUmbracoVersion _version; + private readonly IServerRoleAccessor _serverRoleAccessor; + public SystemInformationTelemetryProvider( IUmbracoVersion version, @@ -33,12 +33,14 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, IOptionsMonitor hostingSettings, IOptionsMonitor globalSettings, IHostEnvironment hostEnvironment, - IUmbracoDatabaseFactory umbracoDatabaseFactory) + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IServerRoleAccessor serverRoleAccessor) { _version = version; _localizationService = localizationService; _hostEnvironment = hostEnvironment; _umbracoDatabaseFactory = umbracoDatabaseFactory; + _serverRoleAccessor = serverRoleAccessor; _globalSettings = globalSettings.CurrentValue; _hostingSettings = hostingSettings.CurrentValue; @@ -63,6 +65,8 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, private string DatabaseProvider => _umbracoDatabaseFactory.CreateDatabase().DatabaseType.GetProviderName(); + private string CurrentServerRole => _serverRoleAccessor.CurrentServerRole.ToString(); + public IEnumerable GetInformation() => new UsageInformation[] { @@ -72,7 +76,8 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, new(Constants.Telemetry.ModelsBuilderMode, ModelsBuilderMode), new(Constants.Telemetry.CustomUmbracoPath, UmbracoPathCustomized), new(Constants.Telemetry.AspEnvironment, AspEnvironment), new(Constants.Telemetry.IsDebug, IsDebug), - new(Constants.Telemetry.DatabaseProvider, DatabaseProvider) + new(Constants.Telemetry.DatabaseProvider, DatabaseProvider), + new(Constants.Telemetry.CurrentServerRole, CurrentServerRole), }; public IEnumerable GetUserData() => @@ -84,7 +89,8 @@ internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, new("Current Culture", CurrentCulture), new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), new("Current Webserver", CurrentWebServer), new("Models Builder Mode", ModelsBuilderMode), - new("Debug Mode", IsDebug.ToString()), new("Database Provider", DatabaseProvider) + new("Debug Mode", IsDebug.ToString()), new("Database Provider", DatabaseProvider), + new("Current Server Role", CurrentServerRole), }; private bool IsRunningInProcessIIS() diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index e898ba49ce..69a961fedd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; @@ -53,6 +53,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Telemetry Constants.Telemetry.AspEnvironment, Constants.Telemetry.IsDebug, Constants.Telemetry.DatabaseProvider, + Constants.Telemetry.CurrentServerRole }; MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs index 3c638fb2ef..243d51378d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Telemetry.Providers; @@ -135,7 +136,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services Mock.Of>(x => x.CurrentValue == new HostingSettings { Debug = isDebug }), Mock.Of>(x => x.CurrentValue == new GlobalSettings()), Mock.Of(), - Mock.Of(x=>x.CreateDatabase() == Mock.Of(y=>y.DatabaseType == DatabaseType.SQLite))); + Mock.Of(x=>x.CreateDatabase() == Mock.Of(y=>y.DatabaseType == DatabaseType.SQLite)), + Mock.Of()); } private ILocalizationService CreateILocalizationService(string culture) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs index 42aae01281..412e0db6fc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Globalization; using System.Linq; using System.Threading; @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Telemetry.Providers; @@ -116,7 +117,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry Mock.Of>(x => x.CurrentValue == new HostingSettings { Debug = isDebug }), Mock.Of>(x => x.CurrentValue == new GlobalSettings{ UmbracoPath = umbracoPath }), hostEnvironment.Object, - Mock.Of(x=>x.CreateDatabase() == Mock.Of(y=>y.DatabaseType == DatabaseType.SQLite))); + Mock.Of(x=>x.CreateDatabase() == Mock.Of(y=>y.DatabaseType == DatabaseType.SQLite)), + Mock.Of()); } } } From f2a98309b4ee772bcdd4afcd61b8bba7fde95195 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 4 Jul 2022 13:45:33 +0200 Subject: [PATCH 043/101] Support localization for content search (backoffice) (#12618) * Support localization for content search * Formatting Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * Review changes * Review changes Co-authored-by: kjac Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Trees/ISearchableTreeWithCulture.cs | 21 +++++++++++++++++++ .../Controllers/EntityController.cs | 14 +++++++++++-- .../Trees/ContentTreeController.cs | 7 +++++-- 3 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs diff --git a/src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs b/src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs new file mode 100644 index 0000000000..6ac4ea3be1 --- /dev/null +++ b/src/Umbraco.Core/Trees/ISearchableTreeWithCulture.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Trees +{ + [Obsolete("This interface will be merged into ISearchableTree in Umbraco 12")] + public interface ISearchableTreeWithCulture : ISearchableTree + { + /// + /// Searches for results based on the entity type + /// + /// + /// + /// + /// + /// A starting point for the search, generally a node id, but for members this is a member type alias + /// + /// + /// + Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null, string? culture = null); + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs index a3716f53aa..08286108c7 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs @@ -204,6 +204,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return result; } + var culture = ClientCulture(); var allowedSections = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.AllowedSections.ToArray(); var searchTasks = new List(); @@ -221,7 +222,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var rootNodeDisplayName = Tree.GetRootNodeDisplayName(tree, _localizedTextService); if (rootNodeDisplayName is not null) { - searchTasks.Add(ExecuteSearchAsync(query, searchableTree, rootNodeDisplayName,result)); + searchTasks.Add(ExecuteSearchAsync(query, culture, searchableTree, rootNodeDisplayName,result)); } } } @@ -232,13 +233,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private static async Task ExecuteSearchAsync( string query, + string? culture, KeyValuePair searchableTree, string rootNodeDisplayName, ConcurrentDictionary result) { + ISearchableTree searcher = searchableTree.Value.SearchableTree; + const int pageSize = 200; + IEnumerable results = ( + searcher is ISearchableTreeWithCulture searcherWithCulture + ? await searcherWithCulture.SearchAsync(query, pageSize, 0, culture: culture) + : await searcher.SearchAsync(query, pageSize, 0)) + .WhereNotNull(); + var searchResult = new TreeSearchResult { - Results = (await searchableTree.Value.SearchableTree.SearchAsync(query, 200, 0)).WhereNotNull(), + Results = results, TreeAlias = searchableTree.Key, AppAlias = searchableTree.Value.AppAlias, JsFormatterService = searchableTree.Value.FormatterService, diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 4b61ef2e17..4fccfb6e24 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -32,7 +32,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees [PluginController(Constants.Web.Mvc.BackOfficeTreeArea)] [CoreTree] [SearchableTree("searchResultFormatter", "configureContentResult", 10)] - public class ContentTreeController : ContentTreeControllerBase, ISearchableTree + public class ContentTreeController : ContentTreeControllerBase, ISearchableTreeWithCulture { private readonly UmbracoTreeSearcher _treeSearcher; private readonly ActionCollection _actions; @@ -369,8 +369,11 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } public async Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null) + => await SearchAsync(query, pageSize, pageIndex, searchFrom, null); + + public async Task SearchAsync(string query, int pageSize, long pageIndex, string? searchFrom = null, string? culture = null) { - var results = _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Document, pageSize, pageIndex, out long totalFound, searchFrom); + var results = _treeSearcher.ExamineSearch(query, UmbracoEntityTypes.Document, pageSize, pageIndex, out long totalFound, culture: culture, searchFrom: searchFrom); return new EntitySearchResults(results, totalFound); } } From 22577163bab721a49a137d1f164f5e0d42d4bad4 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 5 Jul 2022 12:42:52 +0200 Subject: [PATCH 044/101] Remove context from IScopeProvider.cs (#12657) Co-authored-by: Zeegaan --- src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs index 19f07c210c..a21abccbf0 100644 --- a/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs @@ -11,11 +11,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping; ///
public interface IScopeProvider : ICoreScopeProvider { - /// - /// Gets the scope context. - /// - IScopeContext? Context { get; } - /// ICoreScope ICoreScopeProvider.CreateCoreScope( IsolationLevel isolationLevel, From be38fb41adf201991456844c810bddfff8a6adcc Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 5 Jul 2022 15:17:45 +0200 Subject: [PATCH 045/101] V10: Use FireAndForget when logging install (#12658) * Use FireAndForget when logging install * Obsolete constructor Co-authored-by: Zeegaan --- src/Umbraco.Core/FireAndForgetRunner.cs | 41 +++++++++++++++++++ src/Umbraco.Core/IFireAndForgetRunner.cs | 6 +++ .../UmbracoBuilder.CoreServices.cs | 2 + .../Install/InstallHelper.cs | 41 ++++++++++++++++++- 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Core/FireAndForgetRunner.cs create mode 100644 src/Umbraco.Core/IFireAndForgetRunner.cs diff --git a/src/Umbraco.Core/FireAndForgetRunner.cs b/src/Umbraco.Core/FireAndForgetRunner.cs new file mode 100644 index 0000000000..8c466bd439 --- /dev/null +++ b/src/Umbraco.Core/FireAndForgetRunner.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Logging; + +namespace Umbraco.Cms.Core; + + +public class FireAndForgetRunner : IFireAndForgetRunner +{ + private readonly ILogger _logger; + + public FireAndForgetRunner(ILogger logger) => _logger = logger; + + public void RunFireAndForget(Func task) => ExecuteBackgroundTask(task); + + private Task ExecuteBackgroundTask(Func fn) + { + // it is also possible to use UnsafeQueueUserWorkItem which does not flow the execution context, + // however that seems more difficult to use for async operations. + + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + // NOTE: ConfigureAwait(false) is irrelevant here, it is not needed because this is not being + // awaited. ConfigureAwait(false) is only relevant when awaiting to prevent the SynchronizationContext + // (very different from the ExecutionContext!) from running the continuation on the calling thread. + return Task.Run(LoggingWrapper(fn)); + } + } + + private Func LoggingWrapper(Func fn) => + async () => + { + try + { + await fn(); + } + catch (Exception e) + { + _logger.LogError(e, "Exception thrown in a background thread"); + } + }; +} diff --git a/src/Umbraco.Core/IFireAndForgetRunner.cs b/src/Umbraco.Core/IFireAndForgetRunner.cs new file mode 100644 index 0000000000..b28a777990 --- /dev/null +++ b/src/Umbraco.Core/IFireAndForgetRunner.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core; + +public interface IFireAndForgetRunner +{ + void RunFireAndForget(Func task); +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 1c874b2efa..154bae9cd0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -219,6 +219,8 @@ public static partial class UmbracoBuilderExtensions // Services required to run background jobs (with out the handler) builder.Services.AddSingleton(); + + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 41e9d13ed8..1cec345110 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -9,6 +10,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -24,8 +26,35 @@ namespace Umbraco.Cms.Infrastructure.Install private readonly ICookieManager _cookieManager; private readonly IUserAgentProvider _userAgentProvider; private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private readonly IFireAndForgetRunner _fireAndForgetRunner; private InstallationType? _installationType; + public InstallHelper( + DatabaseBuilder databaseBuilder, + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptionsMonitor connectionStrings, + IInstallationService installationService, + ICookieManager cookieManager, + IUserAgentProvider userAgentProvider, + IUmbracoDatabaseFactory umbracoDatabaseFactory, + IFireAndForgetRunner fireAndForgetRunner) + { + _logger = logger; + _umbracoVersion = umbracoVersion; + _databaseBuilder = databaseBuilder; + _connectionStrings = connectionStrings; + _installationService = installationService; + _cookieManager = cookieManager; + _userAgentProvider = userAgentProvider; + _umbracoDatabaseFactory = umbracoDatabaseFactory; + _fireAndForgetRunner = fireAndForgetRunner; + + // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. + GetInstallationType(); + } + + [Obsolete("Please use constructor that takes an IFireAndForgetRunner instead, scheduled for removal in Umbraco 12")] public InstallHelper( DatabaseBuilder databaseBuilder, ILogger logger, @@ -35,6 +64,16 @@ namespace Umbraco.Cms.Infrastructure.Install ICookieManager cookieManager, IUserAgentProvider userAgentProvider, IUmbracoDatabaseFactory umbracoDatabaseFactory) + : this( + databaseBuilder, + logger, + umbracoVersion, + connectionStrings, + installationService, + cookieManager, + userAgentProvider, + umbracoDatabaseFactory, + StaticServiceProvider.Instance.GetRequiredService()) { _logger = logger; _umbracoVersion = umbracoVersion; @@ -87,7 +126,7 @@ namespace Umbraco.Cms.Infrastructure.Install userAgent: userAgent, dbProvider: dbProvider); - await _installationService.LogInstall(installLog); + _fireAndForgetRunner.RunFireAndForget(() => _installationService.LogInstall(installLog)); } catch (Exception ex) { From b0b6081bea5d58bc01dd368db2bc09a4c245626b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 5 Jul 2022 15:24:53 +0200 Subject: [PATCH 046/101] Read BootFailed.html file from WebRootFileProvider (#12634) --- .../Middleware/BootFailedMiddleware.cs | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs index ea2c0e02e4..11abf725c2 100644 --- a/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/BootFailedMiddleware.cs @@ -2,31 +2,25 @@ using System.Text; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.DependencyInjection; -using Umbraco.Extensions; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.Common.Middleware; /// -/// Executes when Umbraco booting fails in order to show the problem +/// Executes when Umbraco booting fails in order to show the problem. /// +/// public class BootFailedMiddleware : IMiddleware { private readonly IHostingEnvironment _hostingEnvironment; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IRuntimeState _runtimeState; - public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment) - : this(runtimeState, hostingEnvironment, StaticServiceProvider.Instance.GetRequiredService()) - { - _runtimeState = runtimeState; - _hostingEnvironment = hostingEnvironment; - } - public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment, IWebHostEnvironment webHostEnvironment) { _runtimeState = runtimeState; @@ -34,6 +28,11 @@ public class BootFailedMiddleware : IMiddleware _webHostEnvironment = webHostEnvironment; } + [Obsolete("Use ctor with all params. This will be removed in Umbraco 12")] + public BootFailedMiddleware(IRuntimeState runtimeState, IHostingEnvironment hostingEnvironment) + : this(runtimeState, hostingEnvironment, StaticServiceProvider.Instance.GetRequiredService()) + { } + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { // TODO: It would be possible to redirect to the installer here in debug mode while @@ -53,10 +52,12 @@ public class BootFailedMiddleware : IMiddleware context.Response.Clear(); context.Response.StatusCode = 500; - var file = GetBootErrorFileName(); - - var viewContent = await File.ReadAllTextAsync(file); - await context.Response.WriteAsync(viewContent, Encoding.UTF8); + IFileInfo? fileInfo = GetBootErrorFileInfo(); + if (fileInfo is not null) + { + using var sr = new StreamReader(fileInfo.CreateReadStream(), Encoding.UTF8); + await context.Response.WriteAsync(await sr.ReadToEndAsync(), Encoding.UTF8); + } } } else @@ -65,14 +66,20 @@ public class BootFailedMiddleware : IMiddleware } } - private string GetBootErrorFileName() + private IFileInfo? GetBootErrorFileInfo() { - var fileName = _webHostEnvironment.MapPathWebRoot("~/config/errors/BootFailed.html"); - if (File.Exists(fileName)) + IFileInfo? fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo("config/errors/BootFailed.html"); + if (fileInfo.Exists) { - return fileName; + return fileInfo; } - return _webHostEnvironment.MapPathWebRoot("~/umbraco/views/errors/BootFailed.html"); + fileInfo = _webHostEnvironment.WebRootFileProvider.GetFileInfo("umbraco/views/errors/BootFailed.html"); + if (fileInfo.Exists) + { + return fileInfo; + } + + return null; } } From 05d095914e3b078d4382fb122ee179067763b905 Mon Sep 17 00:00:00 2001 From: Arul Prabakaran Date: Mon, 4 Jul 2022 07:19:36 +0530 Subject: [PATCH 047/101] Umbraco avatar component updated to fit the image --- src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less index c6b9dc7261..3f32386846 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less @@ -11,6 +11,8 @@ font-weight: bold; font-size: 16px; box-sizing: border-box; + object-fit: cover; + object-position: center; } /* Sizes */ From cb4ec89bcfda4f559560ad3312497376060eba87 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Wed, 6 Jul 2022 01:34:24 +0200 Subject: [PATCH 048/101] Allow only unpublish in schedule publish overlay if user has permission (#11827) * Allow only unpublish in schedule publish overlay if user has permission * Adjust logic to newer JS syntax * Fix update configuration * Delete updated property names * Style unpublish date as disabled * Fix css selector for new button element * Make buttons and datepickers more consistent in invariant and variant versions of schedule overlay * Fill half width like in invariant version of schedule overlay * Use native includes instead * return early from getPermissionsForContent Co-authored-by: Nathan Woulfe --- .../services/contenteditinghelper.service.js | 31 +++++ .../less/components/umb-date-time-picker.less | 8 ++ .../src/less/properties.less | 9 +- .../content/content.rights.controller.js | 59 +++++----- .../content/overlays/schedule.controller.js | 26 +++-- .../src/views/content/overlays/schedule.html | 109 ++++++++++++------ .../listview/listview.controller.js | 24 +--- 7 files changed, 171 insertions(+), 95 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index f4c6063a9a..e7ecb5c93c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -506,6 +506,37 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt }, + /** + * @ngdoc method + * @name umbraco.services.contentEditingHelper#getPermissionsForContent + * @methodOf umbraco.services.contentEditingHelper + * @function + * + * @description + * Returns a object with permissions for user. + */ + getPermissionsForContent: function () { + + // Just ensure we do have an editorState + if (!editorState.current) return null; + + // Fetch current node allowed actions for the current user + // This is the current node & not each individual child node in the list + const currentUserPermissions = editorState.current.allowedActions || []; + + // Create a nicer model rather than the funky & hard to remember permissions strings + const currentNodePermissions = { + canCopy: currentUserPermissions.includes('O'), //Magic Char = O + canCreate: currentUserPermissions.includes('C'), //Magic Char = C + canDelete: currentUserPermissions.includes('D'), //Magic Char = D + canMove: currentUserPermissions.includes('M'), //Magic Char = M + canPublish: currentUserPermissions.includes('U'), //Magic Char = U + canUnpublish: currentUserPermissions.includes('Z') //Magic Char = Z + }; + + return currentNodePermissions; + }, + /** * @ngdoc method * @name umbraco.services.contentEditingHelper#reBindChangedProperties diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less index b8084bc435..bee757a90d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less @@ -1,3 +1,11 @@ +umb-date-time-picker { + &.--disabled { + cursor: not-allowed; + pointer-events: none; + user-select: none; + } +} + .flatpickr-calendar { border-radius: @baseBorderRadius; box-shadow: 0 5px 10px 0 rgba(0,0,0,0.16); diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index 7960aae2c9..101faab724 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -47,16 +47,17 @@ flex-direction: row; } -.date-wrapper-mini--checkbox{ +.date-wrapper-mini--checkbox { margin: 0 0 0 28px; } .date-wrapper-mini__date { display: flex; - + flex-direction: column; margin-left: 5px; margin-top: 5px; margin-bottom: 10px; + flex: 1 1 50%; &:first-of-type { margin-left: 0; @@ -67,8 +68,8 @@ } } -.date-wrapper-mini__date .flatpickr-input > a { - +.date-wrapper-mini__date .flatpickr-input > a, +.date-wrapper-mini__date .flatpickr-input > button { display: flex; align-items: center; justify-content: center; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js index 96c2ef0a80..727872c903 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.rights.controller.js @@ -23,12 +23,11 @@ function onInit() { vm.loading = true; - contentResource.getDetailedPermissions($scope.currentNode.id).then(function (userGroups) { + contentResource.getDetailedPermissions($scope.currentNode.id).then(userGroups => { initData(userGroups); vm.loading = false; currentForm = angularHelper.getCurrentForm($scope); }); - } /** @@ -72,7 +71,7 @@ //if no permissions are explicitly set this means we need to show the defaults vm.selectedUserGroup.permissions = vm.selectedUserGroup.defaultPermissions; } - localizationService.localize("defaultdialogs_permissionsSetForGroup", [$scope.currentNode.name, vm.selectedUserGroup.name]).then(function (value) { + localizationService.localize("defaultdialogs_permissionsSetForGroup", [$scope.currentNode.name, vm.selectedUserGroup.name]).then(value => { vm.labels.permissionsSetForGroup = value; }); setViewSate("managePermissions"); @@ -160,7 +159,7 @@ permissions: permissionsSave }; - contentResource.savePermissions(saveModel).then(function (userGroups) { + contentResource.savePermissions(saveModel).then(userGroups => { //re-assign model from server since it could have changed initData(userGroups); @@ -168,10 +167,11 @@ // clear dirty state on the form so we don't see the discard changes notification // we use a timeout here because in some cases the initData reformats the userGroups model and triggers a change after the form state was changed $timeout(function() { - if(currentForm) { - currentForm.$dirty = false; + if (currentForm) { + currentForm.$dirty = false; } }); + $scope.dialog.confirmDiscardChanges = false; vm.saveState = "success"; vm.saveSuccces = true; @@ -185,28 +185,33 @@ function closeDialog() { // check if form has been changed. If it has show discard changes notification if (currentForm && currentForm.$dirty) { - localizationService.localizeMany(["prompt_unsavedChanges", "prompt_unsavedChangesWarning", "prompt_discardChanges", "prompt_stay"]).then( - function(values) { - var overlay = { - "view": "default", - "title": values[0], - "content": values[1], - "disableBackdropClick": true, - "disableEscKey": true, - "submitButtonLabel": values[2], - "closeButtonLabel": values[3], - submit: function () { - overlayService.close(); - navigationService.hideDialog(); - }, - close: function () { - overlayService.close(); - } - }; - overlayService.open(overlay); - } - ); + const labelKeys = [ + "prompt_unsavedChanges", + "prompt_unsavedChangesWarning", + "prompt_discardChanges", + "prompt_stay" + ]; + + localizationService.localizeMany(labelKeys).then(values => { + + const overlay = { + view: "default", + title: values[0], + content: values[1], + disableBackdropClick: true, + disableEscKey: true, + submitButtonLabel: values[2], + closeButtonLabel: values[3], + submit: () => { + overlayService.close(); + navigationService.hideDialog(); + }, + close: () => overlayService.close() + }; + + overlayService.open(overlay); + }); } else { navigationService.hideDialog(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js index a69de224dd..18121bb2d3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ScheduleContentController($scope, $timeout, localizationService, dateHelper, userService, contentEditingHelper) { + function ScheduleContentController($scope, $timeout, editorState, localizationService, dateHelper, userService, contentEditingHelper) { var vm = this; @@ -21,11 +21,15 @@ var origDates = []; function onInit() { + + //when this is null, we don't check permissions + vm.currentNodePermissions = contentEditingHelper.getPermissionsForContent(); + vm.canUnpublish = vm.currentNodePermissions ? vm.currentNodePermissions.canUnpublish : true; vm.variants = $scope.model.variants; vm.displayVariants = vm.variants.slice(0);// shallow copy, we dont want to share the array-object(because we will be performing a sort method) but each entry should be shared (because we need validation and notifications). - if(!$scope.model.title) { + if (!$scope.model.title) { localizationService.localize("general_scheduledPublishing").then(value => { $scope.model.title = value; }); @@ -65,18 +69,25 @@ var now = new Date(); var nowFormatted = moment(now).format("YYYY-MM-DD HH:mm"); - var datePickerConfig = { + const datePickerPublishConfig = { enableTime: true, dateFormat: "Y-m-d H:i", time_24hr: true, minDate: nowFormatted, - defaultDate: nowFormatted + defaultDate: nowFormatted, + clickOpens: true }; + + const datePickerUnpublishConfig = Utilities.extend({}, datePickerPublishConfig, { + clickOpens: vm.canUnpublish + }); - variant.datePickerConfig = datePickerConfig; + + variant.datePickerPublishConfig = datePickerPublishConfig; + variant.datePickerUnpublishConfig = datePickerUnpublishConfig; // format all dates to local - if(variant.releaseDate || variant.expireDate) { + if (variant.releaseDate || variant.expireDate) { formatDatesToLocal(variant); } }); @@ -348,7 +359,8 @@ // remove properties only needed for this dialog delete variant.releaseDateFormatted; delete variant.expireDateFormatted; - delete variant.datePickerConfig; + delete variant.datePickerPublishConfig; + delete variant.datePickerUnpublishConfig; delete variant.releaseDatePickerInstance; delete variant.expireDatePickerInstance; delete variant.releaseDatePickerOpen; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index f383fb200a..8fca0973e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -15,15 +15,19 @@
- +
- @@ -34,7 +38,11 @@ - @@ -47,27 +55,41 @@ Unpublish at -
- +
+
- -
- @@ -119,14 +141,18 @@
Publish:  {{variant.releaseDateFormatted}}
- +
- @@ -135,7 +161,11 @@
-
@@ -144,15 +174,20 @@
Unpublish:  {{variant.expireDateFormatted}}
-
- +
+
- @@ -161,7 +196,11 @@
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index c3c5dff69b..d6c6cbd451 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,4 +1,4 @@ -function listViewController($scope, $interpolate, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper, eventsService) { +function listViewController($scope, $interpolate, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, contentEditingHelper, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper, eventsService) { //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content // that isn't created yet, if we continue this will use the parent id in the route params which isn't what @@ -67,28 +67,8 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time $scope.createAllowedButtonSingleWithBlueprints = false; $scope.createAllowedButtonMultiWithBlueprints = false; - //when this is null, we don't check permissions - $scope.currentNodePermissions = null; - - if ($scope.entityType === "content") { - //Just ensure we do have an editorState - if (editorState.current) { - //Fetch current node allowed actions for the current user - //This is the current node & not each individual child node in the list - var currentUserPermissions = editorState.current.allowedActions; - - //Create a nicer model rather than the funky & hard to remember permissions strings - $scope.currentNodePermissions = { - "canCopy": _.contains(currentUserPermissions, 'O'), //Magic Char = O - "canCreate": _.contains(currentUserPermissions, 'C'), //Magic Char = C - "canDelete": _.contains(currentUserPermissions, 'D'), //Magic Char = D - "canMove": _.contains(currentUserPermissions, 'M'), //Magic Char = M - "canPublish": _.contains(currentUserPermissions, 'U'), //Magic Char = U - "canUnpublish": _.contains(currentUserPermissions, 'U') //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish) - }; - } - } + $scope.currentNodePermissions = $scope.entityType === "content" ? contentEditingHelper.getPermissionsForContent() : null; //when this is null, we don't check permissions $scope.buttonPermissions = null; From 0fe894c4f66ae9f7f5c21c23a9c4e6dc2f21a3cc Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 6 Jul 2022 11:33:33 +0200 Subject: [PATCH 049/101] Fix up system information test (#12660) Co-authored-by: Zeegaan --- .../integration/HelpPanel/systemInformation.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts index dbc9d19427..8bed0bf7ce 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts @@ -20,7 +20,22 @@ context('System Information', () => { it('Check System Info Displays', () => { openSystemInformation(); - cy.get('.table').find('tr').should('have.length', 13); + + // Assert + cy.get('.table').find('tr').should('contain', 'Server OS'); + cy.get('.table').find('tr').should('contain', 'Server Framework'); + cy.get('.table').find('tr').should('contain', 'Default Language'); + cy.get('.table').find('tr').should('contain', 'Umbraco Version'); + cy.get('.table').find('tr').should('contain', 'Current Culture'); + cy.get('.table').find('tr').should('contain', 'Current UI Culture'); + cy.get('.table').find('tr').should('contain', 'Current Webserver'); + cy.get('.table').find('tr').should('contain', 'Models Builder Mode'); + cy.get('.table').find('tr').should('contain', 'Debug Mode'); + cy.get('.table').find('tr').should('contain', 'Database Provider'); + cy.get('.table').find('tr').should('contain', 'Current Server Role'); + cy.get('.table').find('tr').should('contain', 'Browser'); + cy.get('.table').find('tr').should('contain', 'Browser OS'); + cy.contains('Default Language').parent().should('contain', 'en-US'); cy.contains('Current Culture').parent().should('contain', 'en-US'); cy.contains('Current UI Culture').parent().should('contain', 'en-US'); }); From 6d809042806c49d53cd96ff68fd46ff832157d36 Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Wed, 6 Jul 2022 14:52:00 +0200 Subject: [PATCH 050/101] Make InstallHelper moq greedy --- .../AutoFixture/Customizations/UmbracoCustomizations.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs index 45ff7320c2..76643279fa 100644 --- a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs +++ b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Install; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Install; @@ -37,7 +38,8 @@ internal class UmbracoCustomizations : ICustomization .Customize(new ConstructorCustomization(typeof(BackOfficeUserManager), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(MemberManager), new GreedyConstructorQuery())) .Customize(new ConstructorCustomization(typeof(DatabaseSchemaCreatorFactory), new GreedyConstructorQuery())) - .Customize(new ConstructorCustomization(typeof(BackOfficeServerVariables), new GreedyConstructorQuery())); + .Customize(new ConstructorCustomization(typeof(BackOfficeServerVariables), new GreedyConstructorQuery())) + .Customize(new ConstructorCustomization(typeof(InstallHelper), new GreedyConstructorQuery())); // When requesting an IUserStore ensure we actually uses a IUserLockoutStore fixture.Customize>(cc => From 9c5006e5c8e2b8b0488bbb83fecc71e605348dee Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Wed, 6 Jul 2022 14:53:57 +0200 Subject: [PATCH 051/101] Remove body from obsolete constructor --- .../Install/InstallHelper.cs | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Infrastructure/Install/InstallHelper.cs b/src/Umbraco.Infrastructure/Install/InstallHelper.cs index 1cec345110..85839e8757 100644 --- a/src/Umbraco.Infrastructure/Install/InstallHelper.cs +++ b/src/Umbraco.Infrastructure/Install/InstallHelper.cs @@ -64,28 +64,18 @@ namespace Umbraco.Cms.Infrastructure.Install ICookieManager cookieManager, IUserAgentProvider userAgentProvider, IUmbracoDatabaseFactory umbracoDatabaseFactory) - : this( - databaseBuilder, - logger, - umbracoVersion, - connectionStrings, - installationService, - cookieManager, - userAgentProvider, - umbracoDatabaseFactory, - StaticServiceProvider.Instance.GetRequiredService()) + : this( + databaseBuilder, + logger, + umbracoVersion, + connectionStrings, + installationService, + cookieManager, + userAgentProvider, + umbracoDatabaseFactory, + StaticServiceProvider.Instance.GetRequiredService()) { - _logger = logger; - _umbracoVersion = umbracoVersion; - _databaseBuilder = databaseBuilder; - _connectionStrings = connectionStrings; - _installationService = installationService; - _cookieManager = cookieManager; - _userAgentProvider = userAgentProvider; - _umbracoDatabaseFactory = umbracoDatabaseFactory; - // We need to initialize the type already, as we can't detect later, if the connection string is added on the fly. - GetInstallationType(); } public InstallationType GetInstallationType() => _installationType ??= IsBrandNewInstall ? InstallationType.NewInstall : InstallationType.Upgrade; From 7c2a2996a26c2b2c8dbde1c96d8f1f83746b03ed Mon Sep 17 00:00:00 2001 From: Arul Prabakaran Date: Fri, 8 Jul 2022 20:09:55 +0530 Subject: [PATCH 052/101] Fixes alignment issue for "Saved searches" dropdown in log viewer (#12665) * Umbraco avatar component updated to fit the image * Fixed: Broken saved search dropdown in log viewer --- .../src/less/components/umb-logviewer.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less index 32dcccb8da..77bd216317 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less @@ -114,6 +114,16 @@ max-height: 250px; overflow-y: scroll; margin-top: -10px; + + > li { + + > button { + white-space: normal; + flex-wrap: wrap; + column-gap: 5px; + } + + } } } } From 611badfce2062fbec091350d60d720554247e0b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Knippers?= Date: Mon, 11 Jul 2022 15:41:10 +0200 Subject: [PATCH 053/101] Revert incorrect change in Property.PublishValues (#12680) * Revert incorrect change in Property.PublishValues Fix incorrect change made for nullability support which changes the result of the updated expression to `false` instead of `true` when both inputs are `null`. That is, `null?.InvariantEquals(null) ?? false` becomes `false` but in the previous version it was `true` since the `InvariantEquals` extension method simply calls `string.Equals(v1, v2)` which will return `true` when both inputs are `null`. Due to the `?` operator the `InvariantEquals` method is not called anymore and `?? false` turns the expression to `false`. * Update PropertyValidationService.cs Reverts incorrect update for nullability that changes a value of `true` into `false` when both operands are `null`. Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- src/Umbraco.Core/Models/Property.cs | 6 +++--- src/Umbraco.Core/Services/PropertyValidationService.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index 195772be3a..e49edc9217 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Collections; using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; @@ -198,8 +198,8 @@ public class Property : EntityBase, IProperty // and match the specified culture and segment (or anything when '*'). IEnumerable pvalues = _vvalues.Where(x => PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok - (culture == "*" || (x.Value.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Value.Segment?.InvariantEquals(segment) ?? false))) // the segment matches + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value); foreach (IPropertyValue pvalue in pvalues) diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index d93cbd4a7c..fc42b13232 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -182,8 +182,8 @@ public class PropertyValidationService : IPropertyValidationService var pvalues = property.Values.Where(x => x != pvalue && // don't revalidate pvalue property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok - (culture == "*" || (x.Culture?.InvariantEquals(culture) ?? false)) && // the culture matches - (segment == "*" || (x.Segment?.InvariantEquals(segment) ?? false))) // the segment matches + (culture == "*" || x.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Segment.InvariantEquals(segment))) // the segment matches .ToList(); return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue)); From 942c4b336da8b51890ba4b6563adb78dea8933d6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 11 Jul 2022 15:22:33 +0200 Subject: [PATCH 054/101] Fix formatting/style in DictionaryController --- .../Controllers/DictionaryController.cs | 251 +++++++++--------- 1 file changed, 130 insertions(+), 121 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 54bf101395..f4cefcc76b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -43,9 +43,9 @@ public class DictionaryController : BackOfficeNotificationsController private readonly ILocalizedTextService _localizedTextService; private readonly ILogger _logger; private readonly IUmbracoMapper _umbracoMapper; - private readonly IEntityXmlSerializer _serializer; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly PackageDataInstallation _packageDataInstallation; + private readonly IEntityXmlSerializer _serializer; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly PackageDataInstallation _packageDataInstallation; public DictionaryController( ILogger logger, @@ -53,10 +53,10 @@ public class DictionaryController : BackOfficeNotificationsController IBackOfficeSecurityAccessor backofficeSecurityAccessor, IOptionsSnapshot globalSettings, ILocalizedTextService localizedTextService, - IUmbracoMapper umbracoMapper, - IEntityXmlSerializer serializer, - IHostingEnvironment hostingEnvironment, - PackageDataInstallation packageDataInstallation) + IUmbracoMapper umbracoMapper, + IEntityXmlSerializer serializer, + IHostingEnvironment hostingEnvironment, + PackageDataInstallation packageDataInstallation) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); @@ -65,9 +65,9 @@ public class DictionaryController : BackOfficeNotificationsController _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); - _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); + _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); } /// @@ -397,125 +397,134 @@ public class DictionaryController : BackOfficeNotificationsController } } - public IActionResult ExportDictionary(int id, bool includeChildren = false) + public IActionResult ExportDictionary(int id, bool includeChildren = false) + { + IDictionaryItem? dictionaryItem = _localizationService.GetDictionaryItemById(id); + if (dictionaryItem == null) { - var dictionaryItem = _localizationService.GetDictionaryItemById(id); - if (dictionaryItem == null) - throw new NullReferenceException("No dictionary item found with id " + id); - - var xml = _serializer.Serialize(dictionaryItem, includeChildren); - - var fileName = $"{dictionaryItem.ItemKey}.udt"; - // Set custom header so umbRequestHelper.downloadFile can save the correct filename - HttpContext.Response.Headers.Add("x-filename", fileName); - - return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName); + throw new NullReferenceException("No dictionary item found with id " + id); } - public IActionResult ImportDictionary(string file, int parentId) + XElement xml = _serializer.Serialize(dictionaryItem, includeChildren); + + var fileName = $"{dictionaryItem.ItemKey}.udt"; + // Set custom header so umbRequestHelper.downloadFile can save the correct filename + HttpContext.Response.Headers.Add("x-filename", fileName); + + return File(Encoding.UTF8.GetBytes(xml.ToDataString()), MediaTypeNames.Application.Octet, fileName); + } + + public IActionResult ImportDictionary(string file, int parentId) + { + if (string.IsNullOrWhiteSpace(file)) { - if (string.IsNullOrWhiteSpace(file)) - return NotFound(); - - var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); - if (!System.IO.File.Exists(filePath)) - return NotFound(); - - var xd = new XmlDocument { XmlResolver = null }; - xd.Load(filePath); - - var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0; - var element = XElement.Parse(xd.InnerXml); - - var parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId); - var dictionaryItems = _packageDataInstallation.ImportDictionaryItem(element, userId, parentDictionaryItem?.Key); - - // Try to clean up the temporary file. - try - { - System.IO.File.Delete(filePath); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath); - } - - var model = _umbracoMapper.Map(dictionaryItems.FirstOrDefault()); - return Content(model!.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); + return NotFound(); } - public ActionResult Upload(IFormFile file) + var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file); + if (!System.IO.File.Exists(filePath)) { - - if (file == null) - return ValidationProblem( - _localizedTextService.Localize("media", "failedFileUpload"), - _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); - - var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote); - var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); - var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); - var tempPath = Path.Combine(root, fileName); - - if (!Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root))) - return ValidationProblem( - _localizedTextService.Localize("media", "failedFileUpload"), - _localizedTextService.Localize("media", "invalidFileName")); - - if (!ext.InvariantEquals("udt")) - return ValidationProblem( - _localizedTextService.Localize("media", "failedFileUpload"), - _localizedTextService.Localize("media", "disallowedFileType")); - - using (var stream = System.IO.File.Create(tempPath)) - { - file.CopyToAsync(stream).GetAwaiter().GetResult(); - } - - var xd = new XmlDocument - { - XmlResolver = null - }; - xd.Load(tempPath); - - if (xd.DocumentElement == null) - return ValidationProblem( - _localizedTextService.Localize("media", "failedFileUpload"), - _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); - - DictionaryImportModel model = new DictionaryImportModel() - { - TempFileName = tempPath, - DictionaryItems = new List() - }; - - int level = 1; - string curentParrent = string.Empty; - foreach (XmlNode dictionaryItem in xd.GetElementsByTagName("DictionaryItem")) - { - var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty; - var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; - - if (parentKey != curentParrent || level == 1) - { - level += 1; - curentParrent = parentKey; - } - - model.DictionaryItems.Add(new DictionaryPreviewImportModel() - { - Level = level, - Name = name - }); - } - - if (!model.DictionaryItems.Any()) - return ValidationProblem( - _localizedTextService.Localize("media", "failedFileUpload"), - _localizedTextService.Localize("dictionary", "noItemsInFile")); - - return model; + return NotFound(); } + var xd = new XmlDocument { XmlResolver = null }; + xd.Load(filePath); + + var userId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? 0; + var element = XElement.Parse(xd.InnerXml); + + IDictionaryItem? parentDictionaryItem = _localizationService.GetDictionaryItemById(parentId); + IEnumerable dictionaryItems = _packageDataInstallation + .ImportDictionaryItem(element, userId, parentDictionaryItem?.Key); + + // Try to clean up the temporary file. + try + { + System.IO.File.Delete(filePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error cleaning up temporary udt file in {File}", filePath); + } + + var model = _umbracoMapper.Map(dictionaryItems.FirstOrDefault()); + return Content(model!.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); + } + + public ActionResult Upload(IFormFile file) + { + if (file == null) + { + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); + } + + var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote); + var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); + var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads); + var tempPath = Path.Combine(root, fileName); + + if (!Path.GetFullPath(tempPath).StartsWith(Path.GetFullPath(root))) + { + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("media", "invalidFileName")); + } + + if (!ext.InvariantEquals("udt")) + { + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("media", "disallowedFileType")); + } + + using (FileStream stream = System.IO.File.Create(tempPath)) + { + file.CopyToAsync(stream).GetAwaiter().GetResult(); + } + + var xd = new XmlDocument { XmlResolver = null }; + xd.Load(tempPath); + + if (xd.DocumentElement == null) + { + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("speechBubbles", "fileErrorNotFound")); + } + + var model = new DictionaryImportModel() + { + TempFileName = tempPath, + DictionaryItems = new List(), + }; + + var level = 1; + var currentParent = string.Empty; + foreach (XmlNode dictionaryItem in xd.GetElementsByTagName("DictionaryItem")) + { + var name = dictionaryItem.Attributes?.GetNamedItem("Name")?.Value ?? string.Empty; + var parentKey = dictionaryItem?.ParentNode?.Attributes?.GetNamedItem("Key")?.Value ?? string.Empty; + + if (parentKey != currentParent || level == 1) + { + level += 1; + currentParent = parentKey; + } + + model.DictionaryItems.Add(new DictionaryPreviewImportModel() { Level = level, Name = name }); + } + + if (!model.DictionaryItems.Any()) + { + return ValidationProblem( + _localizedTextService.Localize("media", "failedFileUpload"), + _localizedTextService.Localize("dictionary", "noItemsInFile")); + } + + return model; + } + private static Func ItemSort() => item => item.ItemKey; } From 885ec99cc48d4a3faa8becf8a87914f4a2390c92 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 11 Jul 2022 15:36:23 +0200 Subject: [PATCH 055/101] Fix breaking change in MenuItemList --- src/Umbraco.Core/Trees/MenuItemList.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index 7ba2bae0f8..de926d54d8 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -24,6 +24,17 @@ public class MenuItemList : List : base(items) => _actionCollection = actionCollection; + /// + /// Adds a menu item with a dictionary which is merged to the AdditionalData bag + /// + /// + /// + /// The used to localize the action name based on its alias + /// Whether or not this action opens a dialog + /// Whether or not this action should use legacy icon prefixed with "icon-" or full icon name is specified. + public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) + where T : IAction => Add(textService, hasSeparator, opensDialog, useLegacyIcon: true); + /// /// Adds a menu item with a dictionary which is merged to the AdditionalData bag /// @@ -35,7 +46,7 @@ public class MenuItemList : List public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false, bool useLegacyIcon = true) where T : IAction { - var item = CreateMenuItem(textService, hasSeparator, opensDialog); + MenuItem? item = CreateMenuItem(textService, hasSeparator, opensDialog, useLegacyIcon); if (item != null) { Add(item); From 7611ba7f734e6c5377950ae3d9f8af39f1e3d4db Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 11 Jul 2022 15:50:58 +0200 Subject: [PATCH 056/101] Remove undefined param --- src/Umbraco.Core/Trees/MenuItemList.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Trees/MenuItemList.cs b/src/Umbraco.Core/Trees/MenuItemList.cs index de926d54d8..cb912a112b 100644 --- a/src/Umbraco.Core/Trees/MenuItemList.cs +++ b/src/Umbraco.Core/Trees/MenuItemList.cs @@ -31,7 +31,6 @@ public class MenuItemList : List /// /// The used to localize the action name based on its alias /// Whether or not this action opens a dialog - /// Whether or not this action should use legacy icon prefixed with "icon-" or full icon name is specified. public MenuItem? Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) where T : IAction => Add(textService, hasSeparator, opensDialog, useLegacyIcon: true); From 3778f0da7a1586667ea2771e37331323aea3397f Mon Sep 17 00:00:00 2001 From: Johan Runsten Date: Tue, 12 Jul 2022 14:45:40 +0200 Subject: [PATCH 057/101] Bump ImageSharp to latest version (#12651) --- src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 2 +- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index fc41bec6c5..7d48468a96 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index e08f45e5af..e6b56277bd 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -40,7 +40,9 @@ - + + + From 324f4c7168523338282969ea28fb0e6c99938666 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 12 Jul 2022 14:52:46 +0200 Subject: [PATCH 058/101] Remove accidental merge problem --- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index e6b56277bd..c75ddfe98f 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -41,8 +41,6 @@ - - From af7d9db32bbafb12da13c723b71a0c28a87d0e3d Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 12 Jul 2022 14:02:59 +0200 Subject: [PATCH 059/101] Add missing null-check --- .../Implement/CreatedPackageSchemaRepository.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs index 04c60261ea..9f921266ca 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -652,10 +652,13 @@ public class CreatedPackageSchemaRepository : ICreatedPackagesRepository // the media file path is different from the URL and is specifically // extracted using the property editor for this media file and the current media file system. Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); - xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath!)); + if (mediaFilePath is not null) + { + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); - // add the stream to our outgoing stream - mediaStreams.Add(mediaFilePath!, mediaStream); + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } } IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); From ce568343ab613a8b7cf29e335aad71fc92e03fe4 Mon Sep 17 00:00:00 2001 From: Matthew Care Date: Thu, 23 Jun 2022 00:36:37 +0200 Subject: [PATCH 060/101] Issue 12551 search bug Previous optimisation incorrectly filtered the index fields needed to return published results --- src/Umbraco.Infrastructure/PublishedContentQuery.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index 891f938a2b..e8f25aeae1 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -24,6 +24,9 @@ public class PublishedContentQuery : IPublishedContentQuery private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; + private static readonly HashSet s_returnedQueryFields = + new() { ExamineFieldNames.ItemIdFieldName, ExamineFieldNames.CategoryFieldName }; + /// /// Initializes a new instance of the class. /// @@ -293,8 +296,8 @@ public class PublishedContentQuery : IPublishedContentQuery ordering = query.ManagedQuery(term, fields); } - // Only select item ID field, because results are loaded from the published snapshot based on this single value - IOrdering? queryExecutor = ordering.SelectFields(_itemIdFieldNameHashSet); + // Filter selected fields because results are loaded from the published snapshot based on these + IOrdering? queryExecutor = ordering.SelectFields(s_returnedQueryFields); ISearchResults? results = skip == 0 && take == 0 @@ -328,8 +331,8 @@ public class PublishedContentQuery : IPublishedContentQuery if (query is IOrdering ordering) { - // Only select item ID field, because results are loaded from the published snapshot based on this single value - query = ordering.SelectFields(_itemIdFieldNameHashSet); + // Filter selected fields because results are loaded from the published snapshot based on these + query = ordering.SelectFields(s_returnedQueryFields); } ISearchResults? results = skip == 0 && take == 0 From b8ef3836a1b7d33324b9df6aaed665894e3027bf Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 13 Jul 2022 11:14:31 +0200 Subject: [PATCH 061/101] Fix formatting --- src/Umbraco.Infrastructure/PublishedContentQuery.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index e8f25aeae1..f0518b777d 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -23,8 +23,7 @@ public class PublishedContentQuery : IPublishedContentQuery private readonly IExamineManager _examineManager; private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; - - private static readonly HashSet s_returnedQueryFields = + private static readonly HashSet _returnedQueryFields = new() { ExamineFieldNames.ItemIdFieldName, ExamineFieldNames.CategoryFieldName }; /// @@ -297,7 +296,7 @@ public class PublishedContentQuery : IPublishedContentQuery } // Filter selected fields because results are loaded from the published snapshot based on these - IOrdering? queryExecutor = ordering.SelectFields(s_returnedQueryFields); + IOrdering? queryExecutor = ordering.SelectFields(_returnedQueryFields); ISearchResults? results = skip == 0 && take == 0 @@ -332,7 +331,7 @@ public class PublishedContentQuery : IPublishedContentQuery if (query is IOrdering ordering) { // Filter selected fields because results are loaded from the published snapshot based on these - query = ordering.SelectFields(s_returnedQueryFields); + query = ordering.SelectFields(_returnedQueryFields); } ISearchResults? results = skip == 0 && take == 0 From 3e464933fc21869261abd45fc6e65047c80a48e1 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 13 Jul 2022 11:14:51 +0200 Subject: [PATCH 062/101] delete unused static leftover from merge --- src/Umbraco.Infrastructure/PublishedContentQuery.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/PublishedContentQuery.cs b/src/Umbraco.Infrastructure/PublishedContentQuery.cs index f0518b777d..ef16aeccc6 100644 --- a/src/Umbraco.Infrastructure/PublishedContentQuery.cs +++ b/src/Umbraco.Infrastructure/PublishedContentQuery.cs @@ -18,8 +18,6 @@ namespace Umbraco.Cms.Infrastructure; /// public class PublishedContentQuery : IPublishedContentQuery { - private static readonly HashSet _itemIdFieldNameHashSet = new() {ExamineFieldNames.ItemIdFieldName}; - private readonly IExamineManager _examineManager; private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; From c6f15341483728c7845a99e62156142bda2890e5 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 13 Jul 2022 12:08:23 +0200 Subject: [PATCH 063/101] Change delete action back to move --- src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index 5b01d348d7..27c26004b9 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -180,10 +180,10 @@ public class ContentTypeTreeController : TreeController, ISearchableTree menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); - //no move action if this is a child doc type + // No move action if this is a child doc type if (parent == null) { - menu.Items.Add(LocalizedTextService, hasSeparator: true, opensDialog: true, useLegacyIcon: false); + menu.Items.Add(LocalizedTextService, hasSeparator: true, opensDialog: true, useLegacyIcon: false); } menu.Items.Add(LocalizedTextService, opensDialog: true, useLegacyIcon: false); From 66cd2145adaf4fdee0179b7fcb0e0aafea3e8ca8 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 13 Jul 2022 13:33:42 +0200 Subject: [PATCH 064/101] Reinstate old ctor and obsolete --- .../Controllers/DictionaryController.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index f4cefcc76b..ddc4acb6c2 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -20,6 +20,8 @@ using Umbraco.Extensions; using Umbraco.Cms.Infrastructure.Packaging; using System.Xml.Linq; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Web.BackOffice.Controllers; @@ -60,8 +62,7 @@ public class DictionaryController : BackOfficeNotificationsController { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); - _backofficeSecurityAccessor = backofficeSecurityAccessor ?? - throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); + _backofficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); @@ -70,6 +71,27 @@ public class DictionaryController : BackOfficeNotificationsController _packageDataInstallation = packageDataInstallation ?? throw new ArgumentNullException(nameof(packageDataInstallation)); } + [Obsolete("Please use ctor that also takes an IEntityXmlSerializer, IHostingEnvironment & PackageDataInstallation instead, scheduled for removal in v12")] + public DictionaryController( + ILogger logger, + ILocalizationService localizationService, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IOptionsSnapshot globalSettings, + ILocalizedTextService localizedTextService, + IUmbracoMapper umbracoMapper) + : this( + logger, + localizationService, + backofficeSecurityAccessor, + globalSettings, + localizedTextService, + umbracoMapper, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + /// /// Deletes a data type with a given ID /// From 7f020861c29e981995e7b7cb9022fe4624ca093e Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 13 Jul 2022 14:19:41 +0200 Subject: [PATCH 065/101] Add ActivatorUtilitiesConstructor to ctor --- src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index ddc4acb6c2..fe4e3b2378 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -49,6 +49,7 @@ public class DictionaryController : BackOfficeNotificationsController private readonly IHostingEnvironment _hostingEnvironment; private readonly PackageDataInstallation _packageDataInstallation; + [ActivatorUtilitiesConstructor] public DictionaryController( ILogger logger, ILocalizationService localizationService, From bd2dd3e4424e8a5d292a508488d410ab3538b8ed Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 13 Jul 2022 15:36:56 +0200 Subject: [PATCH 066/101] bumb version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index c5916db03d..11587874da 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.1.0-alpha.1", + "version": "10.1.0-rc", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 920e952e6a31138b562a1aed5cb41916dfee7de7 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 14 Jul 2022 10:13:55 +0200 Subject: [PATCH 067/101] edit pipelines for release --- build/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ae8ca25f52..6289a9bfc4 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -305,7 +305,7 @@ stages: - powershell: sqllocaldb start mssqllocaldb displayName: Start localdb (Windows only) condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - - powershell: docker run --name mssql -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD=$(SA_PASSWORD) -e MSSQL_PID=Developer mcr.microsoft.com/mssql/server:2019-latest + - powershell: docker run --name mssql -d -p 1433:1433 -e ACCEPT_EULA=Y -e SA_PASSWORD=$(SA_PASSWORD) -e MSSQL_PID=Developer mcr.microsoft.com/mssql/server:2019-latest displayName: Start SQL Server (Linux only) condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux')) - task: DotNetCoreCLI@2 @@ -494,7 +494,6 @@ stages: displayName: NuGet release dependsOn: - Deploy_MyGet - - Build_Docs condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.nuGetDeploy}})) jobs: - job: @@ -523,6 +522,7 @@ stages: dependsOn: - Build - Deploy_NuGet + - Build_Docs condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.uploadApiDocs}})) jobs: - job: From e92ceae7ee4a66da42a015da3b8a7d3a7f596a3e Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:31:24 +0200 Subject: [PATCH 068/101] Bumb version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 11587874da..0f9ad08c15 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.1.0-rc", + "version": "10.2.0", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 93225a2c34407fefaa3f8c4a22956583d45c284b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 16:48:01 +0200 Subject: [PATCH 069/101] Add RuntimeModeValidatorCollection and builder --- .../Runtime/RuntimeModeValidatorCollection.cs | 10 ++++++++++ .../Runtime/RuntimeModeValidatorCollectionBuilder.cs | 11 +++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs new file mode 100644 index 0000000000..97f25f3b8f --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +public class RuntimeModeValidatorCollection : BuilderCollectionBase +{ + public RuntimeModeValidatorCollection(Func> items) + : base(items) + { } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs new file mode 100644 index 0000000000..4c755a6040 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +public class RuntimeModeValidatorCollectionBuilder : SetCollectionBuilderBase +{ + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; + + protected override RuntimeModeValidatorCollectionBuilder This => this; +} From 9cec603d46ac5a550332e0c967b5f9a4bb62375d Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 16:48:52 +0200 Subject: [PATCH 070/101] Add RuntimeModeValidators() extension method --- .../UmbracoBuilder.Collections.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs index bc83695d94..609c5305dc 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Runtime; namespace Umbraco.Extensions; @@ -17,6 +18,10 @@ public static partial class UmbracoBuilderExtensions public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + /// + /// Gets the NPoco mappers collection builder. + /// + /// The builder. public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); @@ -26,4 +31,11 @@ public static partial class UmbracoBuilderExtensions /// The builder. public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + /// + /// Gets the runtime mode validators collection builder. + /// + /// The builder. + public static RuntimeModeValidatorCollectionBuilder RuntimeModeValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } From 1fc64cd4ce5c73a9bb68d8b596e8319a7046bc94 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 16:49:49 +0200 Subject: [PATCH 071/101] Swap registration and resolving of IRuntimeModeValidator items to collection builder --- .../UmbracoBuilder.CoreServices.cs | 11 ++++++----- .../Runtime/RuntimeModeValidationService.cs | 13 ++++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 154bae9cd0..62bafcd28e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -86,11 +86,12 @@ public static partial class UmbracoBuilderExtensions // Add runtime mode validation builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.RuntimeModeValidators() + .Add() + .Add() + .Add() + .Add() + .Add(); // composers builder diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs index 85eec91786..c4bbeb6902 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs @@ -29,11 +29,18 @@ internal class RuntimeModeValidationService : IRuntimeModeValidationService var validationMessages = new List(); // Runtime mode validators are registered transient, but this service is registered as singleton - foreach (var runtimeModeValidator in _serviceProvider.GetServices()) + using (var scope = _serviceProvider.CreateScope()) { - if (runtimeModeValidator.Validate(runtimeMode, out var validationMessage) == false) + var runtimeModeValidators = scope.ServiceProvider.GetService(); + if (runtimeModeValidators is not null) { - validationMessages.Add(validationMessage); + foreach (var runtimeModeValidator in runtimeModeValidators) + { + if (runtimeModeValidator.Validate(runtimeMode, out var validationMessage) == false) + { + validationMessages.Add(validationMessage); + } + } } } From db90b16cb18c3436e133c1fd2b8db266133b22ff Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 22:41:20 +0200 Subject: [PATCH 072/101] Skip saving template file when using runtime mode Production --- .../Implement/TemplateRepository.cs | 33 +++++++++++++++---- .../Repositories/ContentTypeRepositoryTest.cs | 5 ++- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Repositories/TemplateRepositoryTest.cs | 3 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index 1a0a90269c..9e01320fdc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -1,8 +1,10 @@ using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -24,17 +26,26 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl { private readonly IIOHelper _ioHelper; private readonly IShortStringHelper _shortStringHelper; - private readonly IViewHelper _viewHelper; private readonly IFileSystem? _viewsFileSystem; + private readonly IViewHelper _viewHelper; + private readonly IOptionsMonitor _runtimeSettings; - public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, - FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) + public TemplateRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + FileSystems fileSystems, + IIOHelper ioHelper, + IShortStringHelper shortStringHelper, + IViewHelper viewHelper, + IOptionsMonitor runtimeSettings) : base(scopeAccessor, cache, logger) { _ioHelper = ioHelper; _shortStringHelper = shortStringHelper; _viewsFileSystem = fileSystems.MvcViewsFileSystem; _viewHelper = viewHelper; + _runtimeSettings = runtimeSettings; } public Stream GetFileContentStream(string filepath) @@ -421,8 +432,12 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set template.Path = nodeDto.Path; - //now do the file work - SaveFile(template); + // Only save file when not in production runtime mode + if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production) + { + //now do the file work + SaveFile(template); + } template.ResetDirtyProperties(); @@ -476,8 +491,12 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl IEnumerable axisDefs = GetAxisDefinitions(dto); template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); - //now do the file work - SaveFile((Template)entity, originalAlias); + // Only save file when not in production runtime mode + if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production) + { + //now do the file work + SaveFile((Template)entity, originalAlias); + } entity.ResetDirtyProperties(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index 3547182538..e2a691a102 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -84,7 +86,8 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest FileSystems, IOHelper, ShortStringHelper, - Mock.Of()); + Mock.Of(), + Mock.Of>()); var repository = ContentTypeRepository; Template[] templates = { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index bcdcf7666a..319470917e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -111,7 +111,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest { appCaches ??= AppCaches; - templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of()); + templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of(), Mock.Of>()); var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index bcf0f185d1..961c11f471 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -59,7 +60,7 @@ public class TemplateRepositoryTest : UmbracoIntegrationTest private IViewHelper ViewHelper => GetRequiredService(); private ITemplateRepository CreateRepository(IScopeProvider provider) => - new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper); + new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, Mock.Of>()); [Test] public void Can_Instantiate_Repository() From 802f2baaa07069d528c0cd43e9bb2c38e73501a2 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 22:48:43 +0200 Subject: [PATCH 073/101] Fix editing template and show warning message when using runtime mode production --- .../EmbeddedResources/Lang/en.xml | 1 + .../EmbeddedResources/Lang/en_us.xml | 1 + .../EmbeddedResources/Lang/nl.xml | 1 + .../Controllers/BackOfficeServerVariables.cs | 2 +- .../src/views/templates/edit.controller.js | 1400 +++++++++-------- .../src/views/templates/edit.html | 10 +- 6 files changed, 715 insertions(+), 700 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 72a1aea35b..49ccf24dd7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1630,6 +1630,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Editor + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 3e2f7c23dc..04883067f6 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1683,6 +1683,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Rich Text Editor + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index a9513c5302..06762e1dd7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -1444,6 +1444,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Rich Text Editor + Production.]]> Kan sjabloon met ID %0% niet verwijderen Sjabloon aanpassen Secties diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index b7fc6a92f5..0681ed957a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -597,7 +597,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { "assemblyVersion", _umbracoVersion.AssemblyVersion?.ToString() } }; - + app.Add("runtimeMode", _runtimeSettings.Mode.ToString()); //the value is the hash of the version, cdf version and the configured state app.Add("cacheBuster", $"{version}.{_runtimeState.Level}.{_runtimeMinifier.CacheBuster}".GenerateHash()); diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 99f11f9913..e231ff51f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -1,723 +1,727 @@ (function () { - "use strict"; + "use strict"; - function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper, templateHelper, editorService) { + function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper, templateHelper, editorService) { - var vm = this; - var oldMasterTemplateAlias = null; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - var id = infiniteMode ? $scope.model.id : $routeParams.id; - var create = infiniteMode ? $scope.model.create : $routeParams.create; + var vm = this; + var oldMasterTemplateAlias = null; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var id = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; - vm.header = {}; - vm.header.editorfor = "template_template"; - vm.header.setPageTitle = true; + vm.runtimeModeProduction = Umbraco.Sys.ServerVariables.application.runtimeMode == 'Production'; - vm.page = {}; - vm.page.loading = true; - vm.templates = []; + vm.header = {}; + vm.header.editorfor = "template_template"; + vm.header.setPageTitle = true; - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; + vm.page = {}; + vm.page.loading = true; + vm.templates = []; - // insert buttons - vm.page.insertDefaultButton = { - labelKey: "general_insert", - addEllipsis: "true", - handler: function () { - vm.openInsertOverlay(); - } - }; - vm.page.insertSubButtons = [ - { - labelKey: "template_insertPageField", - addEllipsis: "true", - handler: function () { - vm.openPageFieldOverlay(); - } - }, - { - labelKey: "template_insertPartialView", - addEllipsis: "true", - handler: function () { - vm.openPartialOverlay(); - } - }, - { - labelKey: "template_insertDictionaryItem", - addEllipsis: "true", - handler: function () { - vm.openDictionaryItemOverlay(); - } - }, - { - labelKey: "template_insertMacro", - addEllipsis: "true", - handler: function () { - vm.openMacroOverlay() - } - } - ]; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; + // insert buttons + vm.page.insertDefaultButton = { + labelKey: "general_insert", + addEllipsis: "true", + handler: function () { + vm.openInsertOverlay(); + } + }; + vm.page.insertSubButtons = [ + { + labelKey: "template_insertPageField", + addEllipsis: "true", + handler: function () { + vm.openPageFieldOverlay(); + } + }, + { + labelKey: "template_insertPartialView", + addEllipsis: "true", + handler: function () { + vm.openPartialOverlay(); + } + }, + { + labelKey: "template_insertDictionaryItem", + addEllipsis: "true", + handler: function () { + vm.openDictionaryItemOverlay(); + } + }, + { + labelKey: "template_insertMacro", + addEllipsis: "true", + handler: function () { + vm.openMacroOverlay() + } + } + ]; - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; - templateHelper.getGeneralShortcuts().then(function (data) { - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getEditorShortcuts().then(function (data) { - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getTemplateEditorShortcuts().then(function (data) { - vm.page.keyboardShortcutsOverview.push(data); + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; + + templateHelper.getGeneralShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getTemplateEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + + vm.save = function (suppressNotification) { + vm.page.saveButtonState = "busy"; + + if (vm.editor) { + vm.template.content = vm.editor.getValue(); + } + + contentEditingHelper.contentEditorPerformSave({ + saveMethod: templateResource.save, + scope: $scope, + content: vm.template, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + + if (!suppressNotification) { + localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.success(header, message); + }); + } + + vm.page.saveButtonState = "success"; + vm.template = saved; + + //sync state + if (!infiniteMode) { + editorState.set(vm.template); + } + + // sync tree + // if master template alias has changed move the node to it's new location + if (!infiniteMode && oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { + + // When creating a new template the id is -1. Make sure We don't remove the root node. + if (vm.page.menu.currentNode.id !== "-1") { + // move node to new location in tree + //first we need to remove the node that we're working on + treeService.removeNode(vm.page.menu.currentNode); + } + + // update stored alias to the new one so the node won't move again unless the alias is changed again + oldMasterTemplateAlias = vm.template.masterTemplateAlias; + + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true, activate: true }).then(function (args) { + vm.page.menu.currentNode = args.node; + }); + + } else { + + // normal tree sync + if (!infiniteMode) { + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + } + + // clear $dirty state on form + setFormState("pristine"); + + if (infiniteMode) { + submit(); + } + + + }, function (err) { + if (suppressNotification) { + vm.page.saveButtonState = "error"; + + localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.error(header, message); + }); + } + }); + + }; + + vm.init = function () { + + // we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + // load templates - used in the master template picker + templateResource.getAll() + .then(function (templates) { + vm.templates = templates; }); - vm.save = function (suppressNotification) { - vm.page.saveButtonState = "busy"; + if (create) { + templateResource.getScaffold((id)).then(function (template) { + vm.ready(template); + }); + } else { + templateResource.getById(id).then(function (template) { + vm.ready(template); + }); + } - vm.template.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - saveMethod: templateResource.save, - scope: $scope, - content: vm.template, - rebindCallback: function (orignal, saved) { } - }).then(function (saved) { - - if (!suppressNotification) { - localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function (data) { - var header = data[0]; - var message = data[1]; - notificationsService.success(header, message); - }); - } - - vm.page.saveButtonState = "success"; - vm.template = saved; - - //sync state - if (!infiniteMode) { - editorState.set(vm.template); - } - - // sync tree - // if master template alias has changed move the node to it's new location - if (!infiniteMode && oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { - - // When creating a new template the id is -1. Make sure We don't remove the root node. - if (vm.page.menu.currentNode.id !== "-1") { - // move node to new location in tree - //first we need to remove the node that we're working on - treeService.removeNode(vm.page.menu.currentNode); - } - - // update stored alias to the new one so the node won't move again unless the alias is changed again - oldMasterTemplateAlias = vm.template.masterTemplateAlias; - - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true, activate: true }).then(function (args) { - vm.page.menu.currentNode = args.node; - }); - - } else { - - // normal tree sync - if (!infiniteMode) { - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - } - - // clear $dirty state on form - setFormState("pristine"); - - if (infiniteMode) { - submit(); - } + }; - }, function (err) { - if (suppressNotification) { - vm.page.saveButtonState = "error"; + vm.ready = function (template) { + vm.page.loading = false; + vm.template = template; - localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function (data) { - var header = data[0]; - var message = data[1]; - notificationsService.error(header, message); - }); - } + // if this is a new template, bind to the blur event on the name + if (create) { + $timeout(function () { + var nameField = $('[data-element="editor-name-field"]'); + if (nameField) { + nameField.on('blur', function (event) { + if (event.target.value) { + vm.save(true); + } }); + } + }); + } + // sync state + if (!infiniteMode) { + editorState.set(vm.template); + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + // save state of master template to use for comparison when syncing the tree on save + oldMasterTemplateAlias = Utilities.copy(template.masterTemplateAlias); + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: false, //The Razor mode snippets are awful (Need a way to override these) + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function (_editor) { + vm.editor = _editor; + + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); + + // Unassigns the keybinding (That was previously auto-complete) + // As conflicts with our own tree search shortcut + _editor.commands.bindKey("ctrl-space", null); + + // Assign new keybinding + _editor.commands.addCommands([ + // Disable (alt+shift+K) + // Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + // Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + + }, + readOnly: true + }, + { + name: 'insertUmbracoValue', + bindKey: 'Alt-Shift-V', + exec: function () { + $scope.$apply(function () { + openPageFieldOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertPartialView', + bindKey: 'Alt-Shift-P', + exec: function () { + $scope.$apply(function () { + openPartialOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertDictionary', + bindKey: 'Alt-Shift-D', + exec: function () { + $scope.$apply(function () { + openDictionaryItemOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoMacro', + bindKey: 'Alt-Shift-M', + exec: function () { + $scope.$apply(function () { + openMacroOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertQuery', + bindKey: 'Alt-Shift-Q', + exec: function () { + $scope.$apply(function () { + openQueryBuilderOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertSection', + bindKey: 'Alt-Shift-S', + exec: function () { + $scope.$apply(function () { + openSectionsOverlay(); + }); + }, + readOnly: true + }, + { + name: 'chooseMasterTemplate', + bindKey: 'Alt-Shift-T', + exec: function () { + $scope.$apply(function () { + openMasterTemplateOverlay(); + }); + }, + readOnly: true + } + + ]); + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + // change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + vm.editor.on("change", changeAceEditor); + } + } + + }; + + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; + vm.openSectionsOverlay = openSectionsOverlay; + vm.openPartialOverlay = openPartialOverlay; + vm.openMasterTemplateOverlay = openMasterTemplateOverlay; + vm.selectMasterTemplate = selectMasterTemplate; + vm.getMasterTemplateName = getMasterTemplateName; + vm.removeMasterTemplate = removeMasterTemplate; + vm.closeShortcuts = closeShortcuts; + vm.submit = submit; + vm.close = close; + + function openInsertOverlay() { + var insertOverlay = { + allowedTypes: { + macro: true, + dictionary: true, + partial: true, + umbracoField: true + }, + submit: function (model) { + switch (model.insert.type) { + case "macro": + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + case "dictionary": + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case "partial": + var code = templateHelper.getInsertPartialSnippet(model.insert.node.parentId, model.insert.node.name); + insert(code); + break; + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + editorService.close(); + }, + close: function (oldModel) { + // close the dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.insertCodeSnippet(insertOverlay); + } + + function openMacroOverlay() { + var macroPicker = { + dialogData: {}, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.macroPicker(macroPicker); + } + + function openPageFieldOverlay() { + var insertFieldEditor = { + submit: function (model) { + insert(model.umbracoField); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.insertField(insertFieldEditor); + } + + + function openDictionaryItemOverlay() { + + var labelKeys = [ + "template_insertDictionaryItem", + "emptyStates_emptyDictionaryTree" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + var title = values[0]; + var emptyStateMessage = values[1]; + + var dictionaryItem = { + section: "translation", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: title, + emptyStateMessage: emptyStateMessage, + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } }; - vm.init = function () { + editorService.treePicker(dictionaryItem); - // we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); - - // load templates - used in the master template picker - templateResource.getAll() - .then(function (templates) { - vm.templates = templates; - }); - - if (create) { - templateResource.getScaffold((id)).then(function (template) { - vm.ready(template); - }); - } else { - templateResource.getById(id).then(function (template) { - vm.ready(template); - }); - } - - }; - - - vm.ready = function (template) { - vm.page.loading = false; - vm.template = template; - - // if this is a new template, bind to the blur event on the name - if (create) { - $timeout(function () { - var nameField = $('[data-element="editor-name-field"]'); - if (nameField) { - nameField.on('blur', function (event) { - if (event.target.value) { - vm.save(true); - } - }); - } - }); - } - - // sync state - if (!infiniteMode) { - editorState.set(vm.template); - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - // save state of master template to use for comparison when syncing the tree on save - oldMasterTemplateAlias = Utilities.copy(template.masterTemplateAlias); - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px', - enableSnippets: false, //The Razor mode snippets are awful (Need a way to override these) - enableBasicAutocompletion: true, - enableLiveAutocompletion: false - }, - onLoad: function (_editor) { - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - // Unassigns the keybinding (That was previously auto-complete) - // As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - // Assign new keybinding - _editor.commands.addCommands([ - // Disable (alt+shift+K) - // Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function () { - // Toggle the show keyboard shortcuts overlay - $scope.$apply(function () { - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - - }, - readOnly: true - }, - { - name: 'insertUmbracoValue', - bindKey: 'Alt-Shift-V', - exec: function () { - $scope.$apply(function () { - openPageFieldOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertPartialView', - bindKey: 'Alt-Shift-P', - exec: function () { - $scope.$apply(function () { - openPartialOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertDictionary', - bindKey: 'Alt-Shift-D', - exec: function () { - $scope.$apply(function () { - openDictionaryItemOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoMacro', - bindKey: 'Alt-Shift-M', - exec: function () { - $scope.$apply(function () { - openMacroOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertQuery', - bindKey: 'Alt-Shift-Q', - exec: function () { - $scope.$apply(function () { - openQueryBuilderOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertSection', - bindKey: 'Alt-Shift-S', - exec: function () { - $scope.$apply(function () { - openSectionsOverlay(); - }); - }, - readOnly: true - }, - { - name: 'chooseMasterTemplate', - bindKey: 'Alt-Shift-T', - exec: function () { - $scope.$apply(function () { - openMasterTemplateOverlay(); - }); - }, - readOnly: true - } - - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if (!create) { - $timeout(function () { - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - // change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - } - } - - }; - - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - vm.openSectionsOverlay = openSectionsOverlay; - vm.openPartialOverlay = openPartialOverlay; - vm.openMasterTemplateOverlay = openMasterTemplateOverlay; - vm.selectMasterTemplate = selectMasterTemplate; - vm.getMasterTemplateName = getMasterTemplateName; - vm.removeMasterTemplate = removeMasterTemplate; - vm.closeShortcuts = closeShortcuts; - vm.submit = submit; - vm.close = close; - - function openInsertOverlay() { - var insertOverlay = { - allowedTypes: { - macro: true, - dictionary: true, - partial: true, - umbracoField: true - }, - submit: function (model) { - switch (model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - case "partial": - var code = templateHelper.getInsertPartialSnippet(model.insert.node.parentId, model.insert.node.name); - insert(code); - break; - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - editorService.close(); - }, - close: function (oldModel) { - // close the dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.insertCodeSnippet(insertOverlay); - } - - function openMacroOverlay() { - var macroPicker = { - dialogData: {}, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.macroPicker(macroPicker); - } - - function openPageFieldOverlay() { - var insertFieldEditor = { - submit: function (model) { - insert(model.umbracoField); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.insertField(insertFieldEditor); - } - - - function openDictionaryItemOverlay() { - - var labelKeys = [ - "template_insertDictionaryItem", - "emptyStates_emptyDictionaryTree" - ]; - - localizationService.localizeMany(labelKeys).then(function (values) { - var title = values[0]; - var emptyStateMessage = values[1]; - - var dictionaryItem = { - section: "translation", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: title, - emptyStateMessage: emptyStateMessage, - select: function (node) { - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - - editorService.treePicker(dictionaryItem); - - }); - - } - - function openPartialOverlay() { - - localizationService.localize("template_insertPartialView").then(function (value) { - var title = value; - - var partialItem = { - section: "settings", - treeAlias: "partialViews", - entityType: "partialView", - multiPicker: false, - title: title, - filter: function (i) { - if (i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { - return true; - } - }, - filterCssClass: "not-allowed", - select: function (node) { - var code = templateHelper.getInsertPartialSnippet(node.parentId, node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - - editorService.treePicker(partialItem); - }); - } - - function openQueryBuilderOverlay() { - var queryBuilder = { - submit: function (model) { - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - editorService.close(); - }, - close: function () { - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.queryBuilder(queryBuilder); - } - - - function openSectionsOverlay() { - var templateSections = { - isMaster: vm.template.isMasterTemplate, - submit: function (model) { - - if (model.insertType === 'renderBody') { - var code = templateHelper.getRenderBodySnippet(); - insert(code); - } - - if (model.insertType === 'renderSection') { - var code = templateHelper.getRenderSectionSnippet(model.renderSectionName, model.mandatoryRenderSection); - insert(code); - } - - if (model.insertType === 'addSection') { - var code = templateHelper.getAddSectionSnippet(model.sectionName); - wrap(code); - } - - editorService.close(); - - }, - close: function (model) { - editorService.close(); - vm.editor.focus(); - } - } - editorService.templateSections(templateSections); - } - - function openMasterTemplateOverlay() { - - // make collection of available master templates - var availableMasterTemplates = []; - - // filter out the current template and the selected master template - vm.templates.forEach(function (template) { - if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { - var templatePathArray = template.path.split(','); - // filter descendant templates of current template - if (templatePathArray.indexOf(String(vm.template.id)) === -1) { - availableMasterTemplates.push(template); - } - } - }); - - const editor = { - filterCssClass: 'not-allowed', - filter: item => !availableMasterTemplates.some(template => template.id == item.id), - submit: model => { - const template = model.selection[0]; - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); - } - editorService.close(); - }, - close: () => editorService.close() - } - - localizationService.localize("template_mastertemplate").then(title => { - editor.title = title; - - const currentTemplate = vm.templates.find(template => template.alias == vm.template.masterTemplateAlias); - if (currentTemplate) { - editor.currentNode = { - path: currentTemplate.path - }; - } - - editorService.templatePicker(editor); - }); - - } - - function selectMasterTemplate(template) { - - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); - } - - } - - function getMasterTemplateName(masterTemplateAlias, templates) { - if (masterTemplateAlias) { - var templateName = ""; - templates.forEach(function (template) { - if (template.alias === masterTemplateAlias) { - templateName = template.name; - } - }); - return templateName; - } - } - - function removeMasterTemplate() { - - vm.template.masterTemplateAlias = null; - - // call set layout with no paramters to set layout to null - setLayout(); - - } - - function setLayout(templatePath) { - - var templateCode = vm.editor.getValue(); - var newValue = templatePath; - var layoutDefRegex = new RegExp("(@{[\\s\\S]*?Layout\\s*?=\\s*?)(\"[^\"]*?\"|null)(;[\\s\\S]*?})", "gi"); - - if (newValue !== undefined && newValue !== "") { - if (layoutDefRegex.test(templateCode)) { - // Declaration exists, so just update it - templateCode = templateCode.replace(layoutDefRegex, "$1\"" + newValue + "\"$3"); - } else { - // Declaration doesn't exist, so prepend to start of doc - // TODO: Maybe insert at the cursor position, rather than just at the top of the doc? - templateCode = "@{\n\tLayout = \"" + newValue + "\";\n}\n" + templateCode; - } - } else { - if (layoutDefRegex.test(templateCode)) { - // Declaration exists, so just update it - templateCode = templateCode.replace(layoutDefRegex, "$1null$3"); - } - } - - vm.editor.setValue(templateCode); - vm.editor.clearSelection(); - vm.editor.navigateFileStart(); - - vm.editor.focus(); - // set form state to $dirty - setFormState("dirty"); - - } - - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function wrap(str) { - - var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); - str = str.replace("{0}", selectedContent); - vm.editor.insert(str); - vm.editor.focus(); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if (state === "dirty") { - currentForm.$setDirty(); - } else if (state === "pristine") { - currentForm.$setPristine(); - } - } - - function closeShortcuts() { - vm.showKeyboardShortcut = false; - } - - function submit() { - if ($scope.model.submit) { - $scope.model.template = vm.template; - $scope.model.submit($scope.model); - } - } - - function close() { - if ($scope.model.close) { - $scope.model.close(); - } - } - - vm.init(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.Templates.EditController", TemplatesEditController); + function openPartialOverlay() { + + localizationService.localize("template_insertPartialView").then(function (value) { + var title = value; + + var partialItem = { + section: "settings", + treeAlias: "partialViews", + entityType: "partialView", + multiPicker: false, + title: title, + filter: function (i) { + if (i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { + return true; + } + }, + filterCssClass: "not-allowed", + select: function (node) { + var code = templateHelper.getInsertPartialSnippet(node.parentId, node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + + editorService.treePicker(partialItem); + }); + } + + function openQueryBuilderOverlay() { + var queryBuilder = { + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + editorService.close(); + }, + close: function () { + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.queryBuilder(queryBuilder); + } + + + function openSectionsOverlay() { + var templateSections = { + isMaster: vm.template.isMasterTemplate, + submit: function (model) { + + if (model.insertType === 'renderBody') { + var code = templateHelper.getRenderBodySnippet(); + insert(code); + } + + if (model.insertType === 'renderSection') { + var code = templateHelper.getRenderSectionSnippet(model.renderSectionName, model.mandatoryRenderSection); + insert(code); + } + + if (model.insertType === 'addSection') { + var code = templateHelper.getAddSectionSnippet(model.sectionName); + wrap(code); + } + + editorService.close(); + + }, + close: function (model) { + editorService.close(); + vm.editor.focus(); + } + } + editorService.templateSections(templateSections); + } + + function openMasterTemplateOverlay() { + + // make collection of available master templates + var availableMasterTemplates = []; + + // filter out the current template and the selected master template + vm.templates.forEach(function (template) { + if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { + var templatePathArray = template.path.split(','); + // filter descendant templates of current template + if (templatePathArray.indexOf(String(vm.template.id)) === -1) { + availableMasterTemplates.push(template); + } + } + }); + + const editor = { + filterCssClass: 'not-allowed', + filter: item => !availableMasterTemplates.some(template => template.id == item.id), + submit: model => { + const template = model.selection[0]; + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + editorService.close(); + }, + close: () => editorService.close() + } + + localizationService.localize("template_mastertemplate").then(title => { + editor.title = title; + + const currentTemplate = vm.templates.find(template => template.alias == vm.template.masterTemplateAlias); + if (currentTemplate) { + editor.currentNode = { + path: currentTemplate.path + }; + } + + editorService.templatePicker(editor); + }); + + } + + function selectMasterTemplate(template) { + + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + + } + + function getMasterTemplateName(masterTemplateAlias, templates) { + if (masterTemplateAlias) { + var templateName = ""; + templates.forEach(function (template) { + if (template.alias === masterTemplateAlias) { + templateName = template.name; + } + }); + return templateName; + } + } + + function removeMasterTemplate() { + + vm.template.masterTemplateAlias = null; + + // call set layout with no paramters to set layout to null + setLayout(); + + } + + function setLayout(templatePath) { + + var templateCode = vm.editor.getValue(); + var newValue = templatePath; + var layoutDefRegex = new RegExp("(@{[\\s\\S]*?Layout\\s*?=\\s*?)(\"[^\"]*?\"|null)(;[\\s\\S]*?})", "gi"); + + if (newValue !== undefined && newValue !== "") { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, "$1\"" + newValue + "\"$3"); + } else { + // Declaration doesn't exist, so prepend to start of doc + // TODO: Maybe insert at the cursor position, rather than just at the top of the doc? + templateCode = "@{\n\tLayout = \"" + newValue + "\";\n}\n" + templateCode; + } + } else { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, "$1null$3"); + } + } + + vm.editor.setValue(templateCode); + vm.editor.clearSelection(); + vm.editor.navigateFileStart(); + + vm.editor.focus(); + // set form state to $dirty + setFormState("dirty"); + + } + + + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + + // set form state to $dirty + setFormState("dirty"); + } + + function wrap(str) { + + var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); + str = str.replace("{0}", selectedContent); + vm.editor.insert(str); + vm.editor.focus(); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if (state === "dirty") { + currentForm.$setDirty(); + } else if (state === "pristine") { + currentForm.$setPristine(); + } + } + + function closeShortcuts() { + vm.showKeyboardShortcut = false; + } + + function submit() { + if ($scope.model.submit) { + $scope.model.template = vm.template; + $scope.model.submit($scope.model); + } + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + vm.init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Templates.EditController", TemplatesEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index f909bc197f..663e0a1f04 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -12,6 +12,7 @@ - + +
+ Template content is not editable when using runtime mode Production. +
+
+ +
@@ -106,6 +113,7 @@ From 97010c2de53d4b5b084aa012482f4e31212d9a4b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 23:41:17 +0200 Subject: [PATCH 074/101] Show editor in read-only mode when using runtime mode production --- .../src/views/templates/edit.controller.js | 9 +++++---- .../src/views/templates/edit.html | 16 +++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index e231ff51f5..1e14cf1e87 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -83,9 +83,7 @@ vm.save = function (suppressNotification) { vm.page.saveButtonState = "busy"; - if (vm.editor) { - vm.template.content = vm.editor.getValue(); - } + vm.template.content = vm.editor.getValue(); contentEditingHelper.contentEditorPerformSave({ saveMethod: templateResource.save, @@ -228,7 +226,10 @@ onLoad: function (_editor) { vm.editor = _editor; - //Update the auto-complete method to use ctrl+alt+space + // Set read-only when using runtime mode Production + _editor.setReadOnly(vm.runtimeModeProduction); + + // Update the auto-complete method to use ctrl+alt+space _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); // Unassigns the keybinding (That was previously auto-complete) diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index 663e0a1f04..5fb4118491 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -23,15 +23,13 @@ - -
- Template content is not editable when using runtime mode Production. -
-
+ + +
+ Template content is not editable when using runtime mode Production. +
- - -
+
@@ -99,7 +97,7 @@
From e581fdc90a6ee678895d7b5d888548d7215d56c2 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 10:34:11 +0200 Subject: [PATCH 075/101] Disable editing partial views and macro files --- .../partialViewMacros/edit.controller.js | 683 +++++++-------- .../src/views/partialViewMacros/edit.html | 118 +-- .../src/views/partialViews/edit.controller.js | 819 +++++++++--------- .../src/views/partialViews/edit.html | 149 ++-- .../src/views/templates/edit.html | 242 +++--- 5 files changed, 1007 insertions(+), 1004 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js index 2f7d879607..d72cdbe69a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js @@ -1,362 +1,367 @@ (function () { - "use strict"; + "use strict"; - function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource, editorService) { + function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource, editorService) { - var vm = this; + var vm = this; - vm.header = {}; - vm.header.editorfor = "visuallyHiddenTexts_newPartialViewMacro"; - vm.header.setPageTitle = true; - vm.page = {}; - vm.page.loading = true; - vm.partialViewMacroFile = {}; + vm.runtimeModeProduction = Umbraco.Sys.ServerVariables.application.runtimeMode == 'Production'; - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; + vm.header = {}; + vm.header.editorfor = "visuallyHiddenTexts_newPartialViewMacro"; + vm.header.setPageTitle = true; + vm.page = {}; + vm.page.loading = true; + vm.partialViewMacroFile = {}; - // insert buttons - vm.page.insertDefaultButton = { - labelKey: "general_insert", - addEllipsis: "true", - handler: function() { - vm.openInsertOverlay(); - } - }; - vm.page.insertSubButtons = [ - { - labelKey: "template_insertPageField", - addEllipsis: "true", - handler: function () { - vm.openPageFieldOverlay(); - } - }, - { - labelKey: "template_insertMacro", - addEllipsis: "true", - handler: function () { - vm.openMacroOverlay() - } - }, - { - labelKey: "template_insertDictionaryItem", - addEllipsis: "true", - handler: function () { - vm.openDictionaryItemOverlay(); - } - } - ]; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; - // bind functions to view model - vm.save = save; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; + // insert buttons + vm.page.insertDefaultButton = { + labelKey: "general_insert", + addEllipsis: "true", + handler: function () { + vm.openInsertOverlay(); + } + }; + vm.page.insertSubButtons = [ + { + labelKey: "template_insertPageField", + addEllipsis: "true", + handler: function () { + vm.openPageFieldOverlay(); + } + }, + { + labelKey: "template_insertMacro", + addEllipsis: "true", + handler: function () { + vm.openMacroOverlay() + } + }, + { + labelKey: "template_insertDictionaryItem", + addEllipsis: "true", + handler: function () { + vm.openDictionaryItemOverlay(); + } + } + ]; - /* Functions bound to view model */ + // bind functions to view model + vm.save = save; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; - function save() { + /* Functions bound to view model */ - vm.page.saveButtonState = "busy"; - vm.partialViewMacro.content = vm.editor.getValue(); + function save() { - contentEditingHelper.contentEditorPerformSave({ - saveMethod: codefileResource.save, - scope: $scope, - content: vm.partialViewMacro, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - // create macro if needed - if($routeParams.create && $routeParams.nomacro !== "true") { - macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function (created) { - navigationService.syncTree({ - tree: "macros", - path: '-1,new', - forceReload: true, - activate: false - }); - completeSave(saved); - }, Utilities.noop); - - - } else { - completeSave(saved); - } - - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); + vm.page.saveButtonState = "busy"; + vm.partialViewMacro.content = vm.editor.getValue(); + contentEditingHelper.contentEditorPerformSave({ + saveMethod: codefileResource.save, + scope: $scope, + content: vm.partialViewMacro, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + // create macro if needed + if ($routeParams.create && $routeParams.nomacro !== "true") { + macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function (created) { + navigationService.syncTree({ + tree: "macros", + path: '-1,new', + forceReload: true, + activate: false }); + completeSave(saved); + }, Utilities.noop); + + } else { + completeSave(saved); } - function completeSave(saved) { + }, function (err) { - localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { - notificationsService.success(headerValue, msgValue); - }); - }); + vm.page.saveButtonState = "error"; - //check if the name changed, if so we need to redirect - if (vm.partialViewMacro.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); - } - else { - vm.page.saveButtonState = "success"; - vm.partialViewMacro = saved; + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); - //sync state - editorState.set(vm.partialViewMacro); - - // normal tree sync - navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - - // clear $dirty state on form - setFormState("pristine"); - } - - } - - function openInsertOverlay() { - var insertOverlay = { - allowedTypes: { - macro: true, - dictionary: true, - umbracoField: true - }, - submit: function(model) { - switch(model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - editorService.close(); - }, - close: function(oldModel) { - // close the dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.insertCodeSnippet(insertOverlay); - } - - function openMacroOverlay() { - var macroPicker = { - dialogData: {}, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - editorService.close(); - }, - close: function() { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.macroPicker(macroPicker); - } - - - function openPageFieldOverlay() { - var insertFieldEditor = { - submit: function (model) { - insert(model.umbracoField); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.insertField(insertFieldEditor); - } - - - function openDictionaryItemOverlay() { - - var labelKeys = [ - "template_insertDictionaryItem", - "emptyStates_emptyDictionaryTree" - ]; - - localizationService.localizeMany(labelKeys).then(function(values){ - var title = values[0]; - var emptyStateMessage = values[1]; - - var dictionaryPicker = { - section: "translation", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: title, - emptyStateMessage: emptyStateMessage, - select: function(node){ - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - - editorService.treePicker(dictionaryPicker); - - }); - } - - function openQueryBuilderOverlay() { - var queryBuilder = { - submit: function (model) { - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.queryBuilder(queryBuilder); - } - - /* Local functions */ - - function init() { - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); - - if ($routeParams.create) { - - var snippet = "Empty"; - - if($routeParams.snippet) { - snippet = $routeParams.snippet; - } - - codefileResource.getScaffold("partialViewMacros", $routeParams.id, snippet).then(function (partialViewMacro) { - if ($routeParams.name) { - partialViewMacro.name = $routeParams.name; - } - ready(partialViewMacro, false); - }); - - } else { - codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { - ready(partialViewMacro, true); - }); - } - } - - function ready(partialViewMacro, syncTree) { - - vm.page.loading = false; - vm.partialViewMacro = partialViewMacro; - - //sync state - editorState.set(vm.partialViewMacro); - - if (syncTree) { - navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px' - }, - onLoad: function(_editor) { - vm.editor = _editor; - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!$routeParams.create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - - } - } - - } - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); - } - } - - - init(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.EditController", partialViewMacrosEditController); + function completeSave(saved) { + + localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { + notificationsService.success(headerValue, msgValue); + }); + }); + + //check if the name changed, if so we need to redirect + if (vm.partialViewMacro.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); + } + else { + vm.page.saveButtonState = "success"; + vm.partialViewMacro = saved; + + //sync state + editorState.set(vm.partialViewMacro); + + // normal tree sync + navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + + // clear $dirty state on form + setFormState("pristine"); + } + + } + + function openInsertOverlay() { + var insertOverlay = { + allowedTypes: { + macro: true, + dictionary: true, + umbracoField: true + }, + submit: function (model) { + switch (model.insert.type) { + case "macro": + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + case "dictionary": + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + editorService.close(); + }, + close: function (oldModel) { + // close the dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.insertCodeSnippet(insertOverlay); + } + + function openMacroOverlay() { + var macroPicker = { + dialogData: {}, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.macroPicker(macroPicker); + } + + + function openPageFieldOverlay() { + var insertFieldEditor = { + submit: function (model) { + insert(model.umbracoField); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.insertField(insertFieldEditor); + } + + + function openDictionaryItemOverlay() { + + var labelKeys = [ + "template_insertDictionaryItem", + "emptyStates_emptyDictionaryTree" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + var title = values[0]; + var emptyStateMessage = values[1]; + + var dictionaryPicker = { + section: "translation", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: title, + emptyStateMessage: emptyStateMessage, + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + + editorService.treePicker(dictionaryPicker); + + }); + } + + function openQueryBuilderOverlay() { + var queryBuilder = { + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.queryBuilder(queryBuilder); + } + + /* Local functions */ + + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + if ($routeParams.create) { + + var snippet = "Empty"; + + if ($routeParams.snippet) { + snippet = $routeParams.snippet; + } + + codefileResource.getScaffold("partialViewMacros", $routeParams.id, snippet).then(function (partialViewMacro) { + if ($routeParams.name) { + partialViewMacro.name = $routeParams.name; + } + ready(partialViewMacro, false); + }); + + } else { + codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { + ready(partialViewMacro, true); + }); + } + } + + function ready(partialViewMacro, syncTree) { + + vm.page.loading = false; + vm.partialViewMacro = partialViewMacro; + + //sync state + editorState.set(vm.partialViewMacro); + + if (syncTree) { + navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px' + }, + onLoad: function (_editor) { + vm.editor = _editor; + + // Set read-only when using runtime mode Production + _editor.setReadOnly(vm.runtimeModeProduction); + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!$routeParams.create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + //change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + vm.editor.on("change", changeAceEditor); + + } + } + + } + + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if (state === "dirty") { + currentForm.$setDirty(); + } else if (state === "pristine") { + currentForm.$setPristine(); + } + } + + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.EditController", partialViewMacrosEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html index 36fff3a044..d15bd866eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html @@ -1,81 +1,81 @@
- + -
+ - + - - + + - - - + + + +
+ Template content is not editable when using runtime mode Production. +
-
+
-
+
- - + + - - + + -
+
-
+
-
-
+
+
-
-
-
+
+
+
- + - + - - + + - + - + -
- +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js index 1e21a63755..512c1176c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js @@ -1,433 +1,438 @@ (function () { - "use strict"; + "use strict"; - function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, editorService) { + function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, editorService) { - var vm = this; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - var id = infiniteMode ? $scope.model.id : $routeParams.id; - var create = infiniteMode ? $scope.model.create : $routeParams.create; - var snippet = infiniteMode ? $scope.model.snippet : $routeParams.snippet; + var vm = this; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var id = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; + var snippet = infiniteMode ? $scope.model.snippet : $routeParams.snippet; - function close() { - if ($scope.model.close) { - $scope.model.close($scope.model); - } + function close() { + if ($scope.model.close) { + $scope.model.close($scope.model); + } + } + + vm.close = close; + + vm.runtimeModeProduction = Umbraco.Sys.ServerVariables.application.runtimeMode == 'Production'; + + vm.header = {}; + vm.header.editorfor = "visuallyHiddenTexts_newPartialView"; + vm.header.setPageTitle = true; + + vm.page = {}; + vm.page.loading = true; + vm.partialView = {}; + + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + + // insert buttons + vm.page.insertDefaultButton = { + labelKey: "general_insert", + addEllipsis: "true", + handler: function () { + vm.openInsertOverlay(); + } + }; + vm.page.insertSubButtons = [ + { + labelKey: "template_insertPageField", + addEllipsis: "true", + handler: function () { + vm.openPageFieldOverlay(); } + }, + { + labelKey: "template_insertMacro", + addEllipsis: "true", + handler: function () { + vm.openMacroOverlay() + } + }, + { + labelKey: "template_insertDictionaryItem", + addEllipsis: "true", + handler: function () { + vm.openDictionaryItemOverlay(); + } + } + ]; - vm.close = close; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; - vm.header = {}; - vm.header.editorfor = "visuallyHiddenTexts_newPartialView"; - vm.header.setPageTitle = true; + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; - vm.page = {}; - vm.page.loading = true; - vm.partialView = {}; + templateHelper.getGeneralShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getPartialViewEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - // insert buttons - vm.page.insertDefaultButton = { - labelKey: "general_insert", - addEllipsis: "true", - handler: function() { - vm.openInsertOverlay(); - } - }; - vm.page.insertSubButtons = [ - { - labelKey: "template_insertPageField", - addEllipsis: "true", - handler: function () { - vm.openPageFieldOverlay(); - } - }, - { - labelKey: "template_insertMacro", - addEllipsis: "true", - handler: function () { - vm.openMacroOverlay() - } - }, - { - labelKey: "template_insertDictionaryItem", - addEllipsis: "true", - handler: function () { - vm.openDictionaryItemOverlay(); - } - } - ]; + // bind functions to view model + vm.save = save; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; + /* Functions bound to view model */ - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; + function save() { - templateHelper.getGeneralShortcuts().then(function(data){ - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getEditorShortcuts().then(function(data){ - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getPartialViewEditorShortcuts().then(function(data){ - vm.page.keyboardShortcutsOverview.push(data); + vm.page.saveButtonState = "busy"; + vm.partialView.content = vm.editor.getValue(); + + contentEditingHelper.contentEditorPerformSave({ + saveMethod: codefileResource.save, + scope: $scope, + content: vm.partialView, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + + localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { + notificationsService.success(headerValue, msgValue); + }); }); - - // bind functions to view model - vm.save = save; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - - /* Functions bound to view model */ - - function save() { - - vm.page.saveButtonState = "busy"; - vm.partialView.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - saveMethod: codefileResource.save, - scope: $scope, - content: vm.partialView, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - - localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_partialViewSavedText").then(function(msgValue) { - notificationsService.success(headerValue, msgValue); - }); - }); - - //check if the name changed, if so we need to redirect - if (vm.partialView.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); - } - else { - vm.page.saveButtonState = "success"; - vm.partialView = saved; - - //sync state - editorState.set(vm.partialView); - - // normal tree sync - navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - - // clear $dirty state on form - setFormState("pristine"); - } - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - - }); - + //check if the name changed, if so we need to redirect + if (vm.partialView.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); } + else { + vm.page.saveButtonState = "success"; + vm.partialView = saved; - function openInsertOverlay() { - var insertOverlay = { - allowedTypes: { - macro: true, - dictionary: true, - umbracoField: true - }, - submit: function(model) { + //sync state + editorState.set(vm.partialView); - switch(model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - editorService.close(); - }, - close: function() { - // close the dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.insertCodeSnippet(insertOverlay); + // normal tree sync + navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + + // clear $dirty state on form + setFormState("pristine"); } + }, function (err) { + vm.page.saveButtonState = "error"; - function openMacroOverlay() { - var macroPicker = { - dialogData: {}, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - editorService.close(); - }, - close: function() { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.macroPicker(macroPicker); - } + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); - function openPageFieldOverlay() { - var insertFieldEditor = { - submit: function (model) { - insert(model.umbracoField); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.insertField(insertFieldEditor); - } - - - function openDictionaryItemOverlay() { - - var labelKeys = [ - "template_insertDictionaryItem", - "emptyStates_emptyDictionaryTree" - ]; - - localizationService.localizeMany(labelKeys).then(function(values){ - var title = values[0]; - var emptyStateMessage = values[1]; - - var dictionaryItem = { - section: "translation", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: title, - emptyStateMessage: emptyStateMessage, - select: function(node){ - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.treePicker(dictionaryItem); - }); - } - - function openQueryBuilderOverlay() { - var queryBuilder = { - title: "Query for content", - submit: function (model) { - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - editorService.close(); - }, - close: function () { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.queryBuilder(queryBuilder); - } - - /* Local functions */ - - function init() { - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); - - if (create) { - - if (!snippet) { - snippet = "Empty"; - } - - codefileResource.getScaffold("partialViews", id, snippet).then(function (partialView) { - ready(partialView, false); - }); - - } else { - codefileResource.getByPath('partialViews', id).then(function (partialView) { - ready(partialView, true); - }); - } - - } - - function ready(partialView, syncTree) { - - vm.page.loading = false; - vm.partialView = partialView; - - //sync state - editorState.set(vm.partialView); - - if (!infiniteMode && syncTree) { - navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px' - }, - onLoad: function(_editor) { - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - //Unassigns the keybinding (That was previously auto-complete) - //As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - // Assign new keybinding - _editor.commands.addCommands([ - //Disable (alt+shift+K) - //Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function () { - //Toggle the show keyboard shortcuts overlay - $scope.$apply(function () { - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoValue', - bindKey: 'Alt-Shift-V', - exec: function () { - $scope.$apply(function () { - openPageFieldOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertDictionary', - bindKey: 'Alt-Shift-D', - exec: function () { - $scope.$apply(function () { - openDictionaryItemOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoMacro', - bindKey: 'Alt-Shift-M', - exec: function () { - $scope.$apply(function () { - openMacroOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertQuery', - bindKey: 'Alt-Shift-Q', - exec: function () { - $scope.$apply(function () { - openQueryBuilderOverlay(); - }); - }, - readOnly: true - } - - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - - } - } - - } - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); - } - } - - - init(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.PartialViews.EditController", PartialViewsEditController); + function openInsertOverlay() { + var insertOverlay = { + allowedTypes: { + macro: true, + dictionary: true, + umbracoField: true + }, + submit: function (model) { + + switch (model.insert.type) { + case "macro": + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + case "dictionary": + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + editorService.close(); + }, + close: function () { + // close the dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.insertCodeSnippet(insertOverlay); + } + + + function openMacroOverlay() { + var macroPicker = { + dialogData: {}, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.macroPicker(macroPicker); + } + + function openPageFieldOverlay() { + var insertFieldEditor = { + submit: function (model) { + insert(model.umbracoField); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.insertField(insertFieldEditor); + } + + + function openDictionaryItemOverlay() { + + var labelKeys = [ + "template_insertDictionaryItem", + "emptyStates_emptyDictionaryTree" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + var title = values[0]; + var emptyStateMessage = values[1]; + + var dictionaryItem = { + section: "translation", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: title, + emptyStateMessage: emptyStateMessage, + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.treePicker(dictionaryItem); + }); + } + + function openQueryBuilderOverlay() { + var queryBuilder = { + title: "Query for content", + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + editorService.close(); + }, + close: function () { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.queryBuilder(queryBuilder); + } + + /* Local functions */ + + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + if (create) { + + if (!snippet) { + snippet = "Empty"; + } + + codefileResource.getScaffold("partialViews", id, snippet).then(function (partialView) { + ready(partialView, false); + }); + + } else { + codefileResource.getByPath('partialViews', id).then(function (partialView) { + ready(partialView, true); + }); + } + + } + + function ready(partialView, syncTree) { + + vm.page.loading = false; + vm.partialView = partialView; + + //sync state + editorState.set(vm.partialView); + + if (!infiniteMode && syncTree) { + navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px' + }, + onLoad: function (_editor) { + vm.editor = _editor; + + // Set read-only when using runtime mode Production + _editor.setReadOnly(vm.runtimeModeProduction); + + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); + + //Unassigns the keybinding (That was previously auto-complete) + //As conflicts with our own tree search shortcut + _editor.commands.bindKey("ctrl-space", null); + + // Assign new keybinding + _editor.commands.addCommands([ + //Disable (alt+shift+K) + //Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + //Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoValue', + bindKey: 'Alt-Shift-V', + exec: function () { + $scope.$apply(function () { + openPageFieldOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertDictionary', + bindKey: 'Alt-Shift-D', + exec: function () { + $scope.$apply(function () { + openDictionaryItemOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoMacro', + bindKey: 'Alt-Shift-M', + exec: function () { + $scope.$apply(function () { + openMacroOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertQuery', + bindKey: 'Alt-Shift-Q', + exec: function () { + $scope.$apply(function () { + openQueryBuilderOverlay(); + }); + }, + readOnly: true + } + + ]); + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + //change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + vm.editor.on("change", changeAceEditor); + + } + } + + } + + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if (state === "dirty") { + currentForm.$setDirty(); + } else if (state === "pristine") { + currentForm.$setPristine(); + } + } + + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PartialViews.EditController", PartialViewsEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html index 4a4d01385e..ec090e6071 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html @@ -1,97 +1,98 @@
- + -
+ - + - - + + - + - - + + +
+ Template content is not editable when using runtime mode Production. +
-
+
-
+
- - + + - - + + -
+
-
+
-
-
+
+
-
-
- -
+ + - +
- - - - + - + + + + - - - - - - + - + + -
- + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index 5fb4118491..dd2cda9a54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -1,150 +1,142 @@
- + -
+ - + - - + + - - - - -
- Template content is not editable when using runtime mode Production. -
+ + -
+ +
+ Template content is not editable when using runtime mode Production. +
-
+
-
+
- +
- + -
+ -
+
-
+
- - +
- - + + - - - -
- -
- -
-
- - - - - - - - - - - - - - - - - + - + - +
- +
-
- +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
From ddfc2fa902d9515376e15a89edc1df1ede888bbb Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 10:45:29 +0200 Subject: [PATCH 076/101] Remove template from warning message --- src/Umbraco.Core/EmbeddedResources/Lang/en.xml | 2 +- src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml | 2 +- src/Umbraco.Core/EmbeddedResources/Lang/nl.xml | 2 +- src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html | 2 +- src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html | 2 +- src/Umbraco.Web.UI.Client/src/views/templates/edit.html | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 49ccf24dd7..d75e70bb07 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1630,7 +1630,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Editor - Production.]]> + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 04883067f6..9641fda47f 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1683,7 +1683,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Rich Text Editor - Production.]]> + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 06762e1dd7..1d76ac06f9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -1444,7 +1444,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Rich Text Editor - Production.]]> + Production.]]> Kan sjabloon met ID %0% niet verwijderen Sjabloon aanpassen Secties diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html index d15bd866eb..7cffa70aa1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html @@ -23,7 +23,7 @@
- Template content is not editable when using runtime mode Production. + Content is not editable when using runtime mode Production.
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html index ec090e6071..e1eb195545 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html @@ -24,7 +24,7 @@
- Template content is not editable when using runtime mode Production. + Content is not editable when using runtime mode Production.
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index dd2cda9a54..a5527095d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -25,7 +25,7 @@
- Template content is not editable when using runtime mode Production. + Content is not editable when using runtime mode Production.
From ae6ee5c3adc4b1087bc5eed03a3cad887804b2cd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 11:07:24 +0200 Subject: [PATCH 077/101] Fix integration test ACE Editor mock --- .../test/unit/app/templates/template-editor-controller.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js index b4615eaad8..9b872f137f 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js @@ -1,4 +1,4 @@ -(function() { +(function() { "use strict"; describe("templates editor controller", @@ -26,6 +26,7 @@ getCursorPosition: function() {}, getValue: function() {}, setValue: function() {}, + setReadOnly: function () { }, focus: function() {}, clearSelection: function() {}, navigateFileStart: function() {}, From f4085efc17cc117335c10787f28beb9cc7f29228 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 14:17:45 +0200 Subject: [PATCH 078/101] Fix integration tests --- .../Persistence/Repositories/ContentTypeRepositoryTest.cs | 5 ++++- .../Persistence/Repositories/DocumentRepositoryTest.cs | 5 ++++- .../Persistence/Repositories/TemplateRepositoryTest.cs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index e2a691a102..aae69e2f61 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -79,6 +79,9 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { + var runtimeSettingsMock = new Mock>(); + runtimeSettingsMock.Setup(x => x.CurrentValue).Returns(new RuntimeSettings()); + var templateRepo = new TemplateRepository( (IScopeAccessor)provider, AppCaches.Disabled, @@ -87,7 +90,7 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest IOHelper, ShortStringHelper, Mock.Of(), - Mock.Of>()); + runtimeSettingsMock.Object); var repository = ContentTypeRepository; Template[] templates = { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 319470917e..9870926544 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -111,7 +111,10 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest { appCaches ??= AppCaches; - templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of(), Mock.Of>()); + var runtimeSettingsMock = new Mock>(); + runtimeSettingsMock.Setup(x => x.CurrentValue).Returns(new RuntimeSettings()); + + templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of(), runtimeSettingsMock.Object); var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index 961c11f471..e0665aaf6d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -57,10 +57,13 @@ public class TemplateRepositoryTest : UmbracoIntegrationTest private IHostingEnvironment HostingEnvironment => GetRequiredService(); private FileSystems FileSystems => GetRequiredService(); + private IViewHelper ViewHelper => GetRequiredService(); + private IOptionsMonitor RuntimeSettings => GetRequiredService>(); + private ITemplateRepository CreateRepository(IScopeProvider provider) => - new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, Mock.Of>()); + new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, RuntimeSettings); [Test] public void Can_Instantiate_Repository() From f11d717522489e141fa3929ed6262373a4b5e005 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 16:48:01 +0200 Subject: [PATCH 079/101] Add RuntimeModeValidatorCollection and builder --- .../Runtime/RuntimeModeValidatorCollection.cs | 10 ++++++++++ .../Runtime/RuntimeModeValidatorCollectionBuilder.cs | 11 +++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs new file mode 100644 index 0000000000..97f25f3b8f --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollection.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +public class RuntimeModeValidatorCollection : BuilderCollectionBase +{ + public RuntimeModeValidatorCollection(Func> items) + : base(items) + { } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs new file mode 100644 index 0000000000..4c755a6040 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidatorCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +public class RuntimeModeValidatorCollectionBuilder : SetCollectionBuilderBase +{ + protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient; + + protected override RuntimeModeValidatorCollectionBuilder This => this; +} From e1c80ecd204f6105a0c9cedbb56f2cf2409169c0 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 16:48:52 +0200 Subject: [PATCH 080/101] Add RuntimeModeValidators() extension method --- .../UmbracoBuilder.Collections.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs index bc83695d94..609c5305dc 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Collections.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Cms.Infrastructure.Runtime; namespace Umbraco.Extensions; @@ -17,6 +18,10 @@ public static partial class UmbracoBuilderExtensions public static MapperCollectionBuilder? Mappers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + /// + /// Gets the NPoco mappers collection builder. + /// + /// The builder. public static NPocoMapperCollectionBuilder? NPocoMappers(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); @@ -26,4 +31,11 @@ public static partial class UmbracoBuilderExtensions /// The builder. public static PackageMigrationPlanCollectionBuilder? PackageMigrationPlans(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + /// + /// Gets the runtime mode validators collection builder. + /// + /// The builder. + public static RuntimeModeValidatorCollectionBuilder RuntimeModeValidators(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); } From df5d657380450556f83ecbdd83a81cbc86fe0878 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 16:49:49 +0200 Subject: [PATCH 081/101] Swap registration and resolving of IRuntimeModeValidator items to collection builder --- .../UmbracoBuilder.CoreServices.cs | 11 ++++++----- .../Runtime/RuntimeModeValidationService.cs | 13 ++++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 154bae9cd0..62bafcd28e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -86,11 +86,12 @@ public static partial class UmbracoBuilderExtensions // Add runtime mode validation builder.Services.AddSingleton(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); - builder.Services.AddTransient(); + builder.RuntimeModeValidators() + .Add() + .Add() + .Add() + .Add() + .Add(); // composers builder diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs index 85eec91786..c4bbeb6902 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs @@ -29,11 +29,18 @@ internal class RuntimeModeValidationService : IRuntimeModeValidationService var validationMessages = new List(); // Runtime mode validators are registered transient, but this service is registered as singleton - foreach (var runtimeModeValidator in _serviceProvider.GetServices()) + using (var scope = _serviceProvider.CreateScope()) { - if (runtimeModeValidator.Validate(runtimeMode, out var validationMessage) == false) + var runtimeModeValidators = scope.ServiceProvider.GetService(); + if (runtimeModeValidators is not null) { - validationMessages.Add(validationMessage); + foreach (var runtimeModeValidator in runtimeModeValidators) + { + if (runtimeModeValidator.Validate(runtimeMode, out var validationMessage) == false) + { + validationMessages.Add(validationMessage); + } + } } } From 9e1e9641a917497606443f7e306466398dcadbe8 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 22:41:20 +0200 Subject: [PATCH 082/101] Skip saving template file when using runtime mode Production --- .../Implement/TemplateRepository.cs | 33 +++++++++++++++---- .../Repositories/ContentTypeRepositoryTest.cs | 5 ++- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Repositories/TemplateRepositoryTest.cs | 3 +- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs index 1a0a90269c..9e01320fdc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs @@ -1,8 +1,10 @@ using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -24,17 +26,26 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl { private readonly IIOHelper _ioHelper; private readonly IShortStringHelper _shortStringHelper; - private readonly IViewHelper _viewHelper; private readonly IFileSystem? _viewsFileSystem; + private readonly IViewHelper _viewHelper; + private readonly IOptionsMonitor _runtimeSettings; - public TemplateRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, - FileSystems fileSystems, IIOHelper ioHelper, IShortStringHelper shortStringHelper, IViewHelper viewHelper) + public TemplateRepository( + IScopeAccessor scopeAccessor, + AppCaches cache, + ILogger logger, + FileSystems fileSystems, + IIOHelper ioHelper, + IShortStringHelper shortStringHelper, + IViewHelper viewHelper, + IOptionsMonitor runtimeSettings) : base(scopeAccessor, cache, logger) { _ioHelper = ioHelper; _shortStringHelper = shortStringHelper; _viewsFileSystem = fileSystems.MvcViewsFileSystem; _viewHelper = viewHelper; + _runtimeSettings = runtimeSettings; } public Stream GetFileContentStream(string filepath) @@ -421,8 +432,12 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl template.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set template.Path = nodeDto.Path; - //now do the file work - SaveFile(template); + // Only save file when not in production runtime mode + if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production) + { + //now do the file work + SaveFile(template); + } template.ResetDirtyProperties(); @@ -476,8 +491,12 @@ internal class TemplateRepository : EntityRepositoryBase, ITempl IEnumerable axisDefs = GetAxisDefinitions(dto); template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); - //now do the file work - SaveFile((Template)entity, originalAlias); + // Only save file when not in production runtime mode + if (_runtimeSettings.CurrentValue.Mode != RuntimeMode.Production) + { + //now do the file work + SaveFile((Template)entity, originalAlias); + } entity.ResetDirtyProperties(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index 3547182538..e2a691a102 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -84,7 +86,8 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest FileSystems, IOHelper, ShortStringHelper, - Mock.Of()); + Mock.Of(), + Mock.Of>()); var repository = ContentTypeRepository; Template[] templates = { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index bcdcf7666a..319470917e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -111,7 +111,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest { appCaches ??= AppCaches; - templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of()); + templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of(), Mock.Of>()); var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index bcf0f185d1..961c11f471 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -59,7 +60,7 @@ public class TemplateRepositoryTest : UmbracoIntegrationTest private IViewHelper ViewHelper => GetRequiredService(); private ITemplateRepository CreateRepository(IScopeProvider provider) => - new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper); + new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, Mock.Of>()); [Test] public void Can_Instantiate_Repository() From 930b21f2734ed7380dd7268775c46963264ccdf7 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 22:48:43 +0200 Subject: [PATCH 083/101] Fix editing template and show warning message when using runtime mode production --- .../EmbeddedResources/Lang/en.xml | 1 + .../EmbeddedResources/Lang/en_us.xml | 1 + .../EmbeddedResources/Lang/nl.xml | 1 + .../Controllers/BackOfficeServerVariables.cs | 2 +- .../src/views/templates/edit.controller.js | 1400 +++++++++-------- .../src/views/templates/edit.html | 10 +- 6 files changed, 715 insertions(+), 700 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 72a1aea35b..49ccf24dd7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1630,6 +1630,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Editor + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 3e2f7c23dc..04883067f6 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1683,6 +1683,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Rich Text Editor + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index a9513c5302..06762e1dd7 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -1444,6 +1444,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Rich Text Editor + Production.]]> Kan sjabloon met ID %0% niet verwijderen Sjabloon aanpassen Secties diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index b7fc6a92f5..0681ed957a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -597,7 +597,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { "assemblyVersion", _umbracoVersion.AssemblyVersion?.ToString() } }; - + app.Add("runtimeMode", _runtimeSettings.Mode.ToString()); //the value is the hash of the version, cdf version and the configured state app.Add("cacheBuster", $"{version}.{_runtimeState.Level}.{_runtimeMinifier.CacheBuster}".GenerateHash()); diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 99f11f9913..e231ff51f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -1,723 +1,727 @@ (function () { - "use strict"; + "use strict"; - function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper, templateHelper, editorService) { + function TemplatesEditController($scope, $routeParams, $timeout, templateResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, treeService, contentEditingHelper, localizationService, angularHelper, templateHelper, editorService) { - var vm = this; - var oldMasterTemplateAlias = null; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - var id = infiniteMode ? $scope.model.id : $routeParams.id; - var create = infiniteMode ? $scope.model.create : $routeParams.create; + var vm = this; + var oldMasterTemplateAlias = null; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var id = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; - vm.header = {}; - vm.header.editorfor = "template_template"; - vm.header.setPageTitle = true; + vm.runtimeModeProduction = Umbraco.Sys.ServerVariables.application.runtimeMode == 'Production'; - vm.page = {}; - vm.page.loading = true; - vm.templates = []; + vm.header = {}; + vm.header.editorfor = "template_template"; + vm.header.setPageTitle = true; - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; + vm.page = {}; + vm.page.loading = true; + vm.templates = []; - // insert buttons - vm.page.insertDefaultButton = { - labelKey: "general_insert", - addEllipsis: "true", - handler: function () { - vm.openInsertOverlay(); - } - }; - vm.page.insertSubButtons = [ - { - labelKey: "template_insertPageField", - addEllipsis: "true", - handler: function () { - vm.openPageFieldOverlay(); - } - }, - { - labelKey: "template_insertPartialView", - addEllipsis: "true", - handler: function () { - vm.openPartialOverlay(); - } - }, - { - labelKey: "template_insertDictionaryItem", - addEllipsis: "true", - handler: function () { - vm.openDictionaryItemOverlay(); - } - }, - { - labelKey: "template_insertMacro", - addEllipsis: "true", - handler: function () { - vm.openMacroOverlay() - } - } - ]; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; + // insert buttons + vm.page.insertDefaultButton = { + labelKey: "general_insert", + addEllipsis: "true", + handler: function () { + vm.openInsertOverlay(); + } + }; + vm.page.insertSubButtons = [ + { + labelKey: "template_insertPageField", + addEllipsis: "true", + handler: function () { + vm.openPageFieldOverlay(); + } + }, + { + labelKey: "template_insertPartialView", + addEllipsis: "true", + handler: function () { + vm.openPartialOverlay(); + } + }, + { + labelKey: "template_insertDictionaryItem", + addEllipsis: "true", + handler: function () { + vm.openDictionaryItemOverlay(); + } + }, + { + labelKey: "template_insertMacro", + addEllipsis: "true", + handler: function () { + vm.openMacroOverlay() + } + } + ]; - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; - templateHelper.getGeneralShortcuts().then(function (data) { - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getEditorShortcuts().then(function (data) { - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getTemplateEditorShortcuts().then(function (data) { - vm.page.keyboardShortcutsOverview.push(data); + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; + + templateHelper.getGeneralShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getTemplateEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + + vm.save = function (suppressNotification) { + vm.page.saveButtonState = "busy"; + + if (vm.editor) { + vm.template.content = vm.editor.getValue(); + } + + contentEditingHelper.contentEditorPerformSave({ + saveMethod: templateResource.save, + scope: $scope, + content: vm.template, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + + if (!suppressNotification) { + localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.success(header, message); + }); + } + + vm.page.saveButtonState = "success"; + vm.template = saved; + + //sync state + if (!infiniteMode) { + editorState.set(vm.template); + } + + // sync tree + // if master template alias has changed move the node to it's new location + if (!infiniteMode && oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { + + // When creating a new template the id is -1. Make sure We don't remove the root node. + if (vm.page.menu.currentNode.id !== "-1") { + // move node to new location in tree + //first we need to remove the node that we're working on + treeService.removeNode(vm.page.menu.currentNode); + } + + // update stored alias to the new one so the node won't move again unless the alias is changed again + oldMasterTemplateAlias = vm.template.masterTemplateAlias; + + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true, activate: true }).then(function (args) { + vm.page.menu.currentNode = args.node; + }); + + } else { + + // normal tree sync + if (!infiniteMode) { + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + } + + // clear $dirty state on form + setFormState("pristine"); + + if (infiniteMode) { + submit(); + } + + + }, function (err) { + if (suppressNotification) { + vm.page.saveButtonState = "error"; + + localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function (data) { + var header = data[0]; + var message = data[1]; + notificationsService.error(header, message); + }); + } + }); + + }; + + vm.init = function () { + + // we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + // load templates - used in the master template picker + templateResource.getAll() + .then(function (templates) { + vm.templates = templates; }); - vm.save = function (suppressNotification) { - vm.page.saveButtonState = "busy"; + if (create) { + templateResource.getScaffold((id)).then(function (template) { + vm.ready(template); + }); + } else { + templateResource.getById(id).then(function (template) { + vm.ready(template); + }); + } - vm.template.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - saveMethod: templateResource.save, - scope: $scope, - content: vm.template, - rebindCallback: function (orignal, saved) { } - }).then(function (saved) { - - if (!suppressNotification) { - localizationService.localizeMany(["speechBubbles_templateSavedHeader", "speechBubbles_templateSavedText"]).then(function (data) { - var header = data[0]; - var message = data[1]; - notificationsService.success(header, message); - }); - } - - vm.page.saveButtonState = "success"; - vm.template = saved; - - //sync state - if (!infiniteMode) { - editorState.set(vm.template); - } - - // sync tree - // if master template alias has changed move the node to it's new location - if (!infiniteMode && oldMasterTemplateAlias !== vm.template.masterTemplateAlias) { - - // When creating a new template the id is -1. Make sure We don't remove the root node. - if (vm.page.menu.currentNode.id !== "-1") { - // move node to new location in tree - //first we need to remove the node that we're working on - treeService.removeNode(vm.page.menu.currentNode); - } - - // update stored alias to the new one so the node won't move again unless the alias is changed again - oldMasterTemplateAlias = vm.template.masterTemplateAlias; - - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true, activate: true }).then(function (args) { - vm.page.menu.currentNode = args.node; - }); - - } else { - - // normal tree sync - if (!infiniteMode) { - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - } - - // clear $dirty state on form - setFormState("pristine"); - - if (infiniteMode) { - submit(); - } + }; - }, function (err) { - if (suppressNotification) { - vm.page.saveButtonState = "error"; + vm.ready = function (template) { + vm.page.loading = false; + vm.template = template; - localizationService.localizeMany(["speechBubbles_validationFailedHeader", "speechBubbles_validationFailedMessage"]).then(function (data) { - var header = data[0]; - var message = data[1]; - notificationsService.error(header, message); - }); - } + // if this is a new template, bind to the blur event on the name + if (create) { + $timeout(function () { + var nameField = $('[data-element="editor-name-field"]'); + if (nameField) { + nameField.on('blur', function (event) { + if (event.target.value) { + vm.save(true); + } }); + } + }); + } + // sync state + if (!infiniteMode) { + editorState.set(vm.template); + navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + // save state of master template to use for comparison when syncing the tree on save + oldMasterTemplateAlias = Utilities.copy(template.masterTemplateAlias); + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px', + enableSnippets: false, //The Razor mode snippets are awful (Need a way to override these) + enableBasicAutocompletion: true, + enableLiveAutocompletion: false + }, + onLoad: function (_editor) { + vm.editor = _editor; + + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); + + // Unassigns the keybinding (That was previously auto-complete) + // As conflicts with our own tree search shortcut + _editor.commands.bindKey("ctrl-space", null); + + // Assign new keybinding + _editor.commands.addCommands([ + // Disable (alt+shift+K) + // Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + // Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + + }, + readOnly: true + }, + { + name: 'insertUmbracoValue', + bindKey: 'Alt-Shift-V', + exec: function () { + $scope.$apply(function () { + openPageFieldOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertPartialView', + bindKey: 'Alt-Shift-P', + exec: function () { + $scope.$apply(function () { + openPartialOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertDictionary', + bindKey: 'Alt-Shift-D', + exec: function () { + $scope.$apply(function () { + openDictionaryItemOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoMacro', + bindKey: 'Alt-Shift-M', + exec: function () { + $scope.$apply(function () { + openMacroOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertQuery', + bindKey: 'Alt-Shift-Q', + exec: function () { + $scope.$apply(function () { + openQueryBuilderOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertSection', + bindKey: 'Alt-Shift-S', + exec: function () { + $scope.$apply(function () { + openSectionsOverlay(); + }); + }, + readOnly: true + }, + { + name: 'chooseMasterTemplate', + bindKey: 'Alt-Shift-T', + exec: function () { + $scope.$apply(function () { + openMasterTemplateOverlay(); + }); + }, + readOnly: true + } + + ]); + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + // change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + vm.editor.on("change", changeAceEditor); + } + } + + }; + + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; + vm.openSectionsOverlay = openSectionsOverlay; + vm.openPartialOverlay = openPartialOverlay; + vm.openMasterTemplateOverlay = openMasterTemplateOverlay; + vm.selectMasterTemplate = selectMasterTemplate; + vm.getMasterTemplateName = getMasterTemplateName; + vm.removeMasterTemplate = removeMasterTemplate; + vm.closeShortcuts = closeShortcuts; + vm.submit = submit; + vm.close = close; + + function openInsertOverlay() { + var insertOverlay = { + allowedTypes: { + macro: true, + dictionary: true, + partial: true, + umbracoField: true + }, + submit: function (model) { + switch (model.insert.type) { + case "macro": + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + case "dictionary": + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case "partial": + var code = templateHelper.getInsertPartialSnippet(model.insert.node.parentId, model.insert.node.name); + insert(code); + break; + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + editorService.close(); + }, + close: function (oldModel) { + // close the dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.insertCodeSnippet(insertOverlay); + } + + function openMacroOverlay() { + var macroPicker = { + dialogData: {}, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.macroPicker(macroPicker); + } + + function openPageFieldOverlay() { + var insertFieldEditor = { + submit: function (model) { + insert(model.umbracoField); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.insertField(insertFieldEditor); + } + + + function openDictionaryItemOverlay() { + + var labelKeys = [ + "template_insertDictionaryItem", + "emptyStates_emptyDictionaryTree" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + var title = values[0]; + var emptyStateMessage = values[1]; + + var dictionaryItem = { + section: "translation", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: title, + emptyStateMessage: emptyStateMessage, + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } }; - vm.init = function () { + editorService.treePicker(dictionaryItem); - // we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); - - // load templates - used in the master template picker - templateResource.getAll() - .then(function (templates) { - vm.templates = templates; - }); - - if (create) { - templateResource.getScaffold((id)).then(function (template) { - vm.ready(template); - }); - } else { - templateResource.getById(id).then(function (template) { - vm.ready(template); - }); - } - - }; - - - vm.ready = function (template) { - vm.page.loading = false; - vm.template = template; - - // if this is a new template, bind to the blur event on the name - if (create) { - $timeout(function () { - var nameField = $('[data-element="editor-name-field"]'); - if (nameField) { - nameField.on('blur', function (event) { - if (event.target.value) { - vm.save(true); - } - }); - } - }); - } - - // sync state - if (!infiniteMode) { - editorState.set(vm.template); - navigationService.syncTree({ tree: "templates", path: vm.template.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - // save state of master template to use for comparison when syncing the tree on save - oldMasterTemplateAlias = Utilities.copy(template.masterTemplateAlias); - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px', - enableSnippets: false, //The Razor mode snippets are awful (Need a way to override these) - enableBasicAutocompletion: true, - enableLiveAutocompletion: false - }, - onLoad: function (_editor) { - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - // Unassigns the keybinding (That was previously auto-complete) - // As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - // Assign new keybinding - _editor.commands.addCommands([ - // Disable (alt+shift+K) - // Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function () { - // Toggle the show keyboard shortcuts overlay - $scope.$apply(function () { - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - - }, - readOnly: true - }, - { - name: 'insertUmbracoValue', - bindKey: 'Alt-Shift-V', - exec: function () { - $scope.$apply(function () { - openPageFieldOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertPartialView', - bindKey: 'Alt-Shift-P', - exec: function () { - $scope.$apply(function () { - openPartialOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertDictionary', - bindKey: 'Alt-Shift-D', - exec: function () { - $scope.$apply(function () { - openDictionaryItemOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoMacro', - bindKey: 'Alt-Shift-M', - exec: function () { - $scope.$apply(function () { - openMacroOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertQuery', - bindKey: 'Alt-Shift-Q', - exec: function () { - $scope.$apply(function () { - openQueryBuilderOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertSection', - bindKey: 'Alt-Shift-S', - exec: function () { - $scope.$apply(function () { - openSectionsOverlay(); - }); - }, - readOnly: true - }, - { - name: 'chooseMasterTemplate', - bindKey: 'Alt-Shift-T', - exec: function () { - $scope.$apply(function () { - openMasterTemplateOverlay(); - }); - }, - readOnly: true - } - - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if (!create) { - $timeout(function () { - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - // change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - } - } - - }; - - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - vm.openSectionsOverlay = openSectionsOverlay; - vm.openPartialOverlay = openPartialOverlay; - vm.openMasterTemplateOverlay = openMasterTemplateOverlay; - vm.selectMasterTemplate = selectMasterTemplate; - vm.getMasterTemplateName = getMasterTemplateName; - vm.removeMasterTemplate = removeMasterTemplate; - vm.closeShortcuts = closeShortcuts; - vm.submit = submit; - vm.close = close; - - function openInsertOverlay() { - var insertOverlay = { - allowedTypes: { - macro: true, - dictionary: true, - partial: true, - umbracoField: true - }, - submit: function (model) { - switch (model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - case "partial": - var code = templateHelper.getInsertPartialSnippet(model.insert.node.parentId, model.insert.node.name); - insert(code); - break; - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - editorService.close(); - }, - close: function (oldModel) { - // close the dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.insertCodeSnippet(insertOverlay); - } - - function openMacroOverlay() { - var macroPicker = { - dialogData: {}, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.macroPicker(macroPicker); - } - - function openPageFieldOverlay() { - var insertFieldEditor = { - submit: function (model) { - insert(model.umbracoField); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.insertField(insertFieldEditor); - } - - - function openDictionaryItemOverlay() { - - var labelKeys = [ - "template_insertDictionaryItem", - "emptyStates_emptyDictionaryTree" - ]; - - localizationService.localizeMany(labelKeys).then(function (values) { - var title = values[0]; - var emptyStateMessage = values[1]; - - var dictionaryItem = { - section: "translation", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: title, - emptyStateMessage: emptyStateMessage, - select: function (node) { - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - - editorService.treePicker(dictionaryItem); - - }); - - } - - function openPartialOverlay() { - - localizationService.localize("template_insertPartialView").then(function (value) { - var title = value; - - var partialItem = { - section: "settings", - treeAlias: "partialViews", - entityType: "partialView", - multiPicker: false, - title: title, - filter: function (i) { - if (i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { - return true; - } - }, - filterCssClass: "not-allowed", - select: function (node) { - var code = templateHelper.getInsertPartialSnippet(node.parentId, node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - - editorService.treePicker(partialItem); - }); - } - - function openQueryBuilderOverlay() { - var queryBuilder = { - submit: function (model) { - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - editorService.close(); - }, - close: function () { - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.queryBuilder(queryBuilder); - } - - - function openSectionsOverlay() { - var templateSections = { - isMaster: vm.template.isMasterTemplate, - submit: function (model) { - - if (model.insertType === 'renderBody') { - var code = templateHelper.getRenderBodySnippet(); - insert(code); - } - - if (model.insertType === 'renderSection') { - var code = templateHelper.getRenderSectionSnippet(model.renderSectionName, model.mandatoryRenderSection); - insert(code); - } - - if (model.insertType === 'addSection') { - var code = templateHelper.getAddSectionSnippet(model.sectionName); - wrap(code); - } - - editorService.close(); - - }, - close: function (model) { - editorService.close(); - vm.editor.focus(); - } - } - editorService.templateSections(templateSections); - } - - function openMasterTemplateOverlay() { - - // make collection of available master templates - var availableMasterTemplates = []; - - // filter out the current template and the selected master template - vm.templates.forEach(function (template) { - if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { - var templatePathArray = template.path.split(','); - // filter descendant templates of current template - if (templatePathArray.indexOf(String(vm.template.id)) === -1) { - availableMasterTemplates.push(template); - } - } - }); - - const editor = { - filterCssClass: 'not-allowed', - filter: item => !availableMasterTemplates.some(template => template.id == item.id), - submit: model => { - const template = model.selection[0]; - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); - } - editorService.close(); - }, - close: () => editorService.close() - } - - localizationService.localize("template_mastertemplate").then(title => { - editor.title = title; - - const currentTemplate = vm.templates.find(template => template.alias == vm.template.masterTemplateAlias); - if (currentTemplate) { - editor.currentNode = { - path: currentTemplate.path - }; - } - - editorService.templatePicker(editor); - }); - - } - - function selectMasterTemplate(template) { - - if (template && template.alias) { - vm.template.masterTemplateAlias = template.alias; - setLayout(template.alias + ".cshtml"); - } else { - vm.template.masterTemplateAlias = null; - setLayout(null); - } - - } - - function getMasterTemplateName(masterTemplateAlias, templates) { - if (masterTemplateAlias) { - var templateName = ""; - templates.forEach(function (template) { - if (template.alias === masterTemplateAlias) { - templateName = template.name; - } - }); - return templateName; - } - } - - function removeMasterTemplate() { - - vm.template.masterTemplateAlias = null; - - // call set layout with no paramters to set layout to null - setLayout(); - - } - - function setLayout(templatePath) { - - var templateCode = vm.editor.getValue(); - var newValue = templatePath; - var layoutDefRegex = new RegExp("(@{[\\s\\S]*?Layout\\s*?=\\s*?)(\"[^\"]*?\"|null)(;[\\s\\S]*?})", "gi"); - - if (newValue !== undefined && newValue !== "") { - if (layoutDefRegex.test(templateCode)) { - // Declaration exists, so just update it - templateCode = templateCode.replace(layoutDefRegex, "$1\"" + newValue + "\"$3"); - } else { - // Declaration doesn't exist, so prepend to start of doc - // TODO: Maybe insert at the cursor position, rather than just at the top of the doc? - templateCode = "@{\n\tLayout = \"" + newValue + "\";\n}\n" + templateCode; - } - } else { - if (layoutDefRegex.test(templateCode)) { - // Declaration exists, so just update it - templateCode = templateCode.replace(layoutDefRegex, "$1null$3"); - } - } - - vm.editor.setValue(templateCode); - vm.editor.clearSelection(); - vm.editor.navigateFileStart(); - - vm.editor.focus(); - // set form state to $dirty - setFormState("dirty"); - - } - - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function wrap(str) { - - var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); - str = str.replace("{0}", selectedContent); - vm.editor.insert(str); - vm.editor.focus(); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if (state === "dirty") { - currentForm.$setDirty(); - } else if (state === "pristine") { - currentForm.$setPristine(); - } - } - - function closeShortcuts() { - vm.showKeyboardShortcut = false; - } - - function submit() { - if ($scope.model.submit) { - $scope.model.template = vm.template; - $scope.model.submit($scope.model); - } - } - - function close() { - if ($scope.model.close) { - $scope.model.close(); - } - } - - vm.init(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.Templates.EditController", TemplatesEditController); + function openPartialOverlay() { + + localizationService.localize("template_insertPartialView").then(function (value) { + var title = value; + + var partialItem = { + section: "settings", + treeAlias: "partialViews", + entityType: "partialView", + multiPicker: false, + title: title, + filter: function (i) { + if (i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { + return true; + } + }, + filterCssClass: "not-allowed", + select: function (node) { + var code = templateHelper.getInsertPartialSnippet(node.parentId, node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + + editorService.treePicker(partialItem); + }); + } + + function openQueryBuilderOverlay() { + var queryBuilder = { + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + editorService.close(); + }, + close: function () { + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.queryBuilder(queryBuilder); + } + + + function openSectionsOverlay() { + var templateSections = { + isMaster: vm.template.isMasterTemplate, + submit: function (model) { + + if (model.insertType === 'renderBody') { + var code = templateHelper.getRenderBodySnippet(); + insert(code); + } + + if (model.insertType === 'renderSection') { + var code = templateHelper.getRenderSectionSnippet(model.renderSectionName, model.mandatoryRenderSection); + insert(code); + } + + if (model.insertType === 'addSection') { + var code = templateHelper.getAddSectionSnippet(model.sectionName); + wrap(code); + } + + editorService.close(); + + }, + close: function (model) { + editorService.close(); + vm.editor.focus(); + } + } + editorService.templateSections(templateSections); + } + + function openMasterTemplateOverlay() { + + // make collection of available master templates + var availableMasterTemplates = []; + + // filter out the current template and the selected master template + vm.templates.forEach(function (template) { + if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { + var templatePathArray = template.path.split(','); + // filter descendant templates of current template + if (templatePathArray.indexOf(String(vm.template.id)) === -1) { + availableMasterTemplates.push(template); + } + } + }); + + const editor = { + filterCssClass: 'not-allowed', + filter: item => !availableMasterTemplates.some(template => template.id == item.id), + submit: model => { + const template = model.selection[0]; + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + editorService.close(); + }, + close: () => editorService.close() + } + + localizationService.localize("template_mastertemplate").then(title => { + editor.title = title; + + const currentTemplate = vm.templates.find(template => template.alias == vm.template.masterTemplateAlias); + if (currentTemplate) { + editor.currentNode = { + path: currentTemplate.path + }; + } + + editorService.templatePicker(editor); + }); + + } + + function selectMasterTemplate(template) { + + if (template && template.alias) { + vm.template.masterTemplateAlias = template.alias; + setLayout(template.alias + ".cshtml"); + } else { + vm.template.masterTemplateAlias = null; + setLayout(null); + } + + } + + function getMasterTemplateName(masterTemplateAlias, templates) { + if (masterTemplateAlias) { + var templateName = ""; + templates.forEach(function (template) { + if (template.alias === masterTemplateAlias) { + templateName = template.name; + } + }); + return templateName; + } + } + + function removeMasterTemplate() { + + vm.template.masterTemplateAlias = null; + + // call set layout with no paramters to set layout to null + setLayout(); + + } + + function setLayout(templatePath) { + + var templateCode = vm.editor.getValue(); + var newValue = templatePath; + var layoutDefRegex = new RegExp("(@{[\\s\\S]*?Layout\\s*?=\\s*?)(\"[^\"]*?\"|null)(;[\\s\\S]*?})", "gi"); + + if (newValue !== undefined && newValue !== "") { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, "$1\"" + newValue + "\"$3"); + } else { + // Declaration doesn't exist, so prepend to start of doc + // TODO: Maybe insert at the cursor position, rather than just at the top of the doc? + templateCode = "@{\n\tLayout = \"" + newValue + "\";\n}\n" + templateCode; + } + } else { + if (layoutDefRegex.test(templateCode)) { + // Declaration exists, so just update it + templateCode = templateCode.replace(layoutDefRegex, "$1null$3"); + } + } + + vm.editor.setValue(templateCode); + vm.editor.clearSelection(); + vm.editor.navigateFileStart(); + + vm.editor.focus(); + // set form state to $dirty + setFormState("dirty"); + + } + + + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + + // set form state to $dirty + setFormState("dirty"); + } + + function wrap(str) { + + var selectedContent = vm.editor.session.getTextRange(vm.editor.getSelectionRange()); + str = str.replace("{0}", selectedContent); + vm.editor.insert(str); + vm.editor.focus(); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if (state === "dirty") { + currentForm.$setDirty(); + } else if (state === "pristine") { + currentForm.$setPristine(); + } + } + + function closeShortcuts() { + vm.showKeyboardShortcut = false; + } + + function submit() { + if ($scope.model.submit) { + $scope.model.template = vm.template; + $scope.model.submit($scope.model); + } + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + vm.init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Templates.EditController", TemplatesEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index f909bc197f..663e0a1f04 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -12,6 +12,7 @@ - + +
+ Template content is not editable when using runtime mode Production. +
+
+ +
@@ -106,6 +113,7 @@ From 40eae24a552c2c4de8c180c3600bd3b9f36089a2 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 14 Jul 2022 23:41:17 +0200 Subject: [PATCH 084/101] Show editor in read-only mode when using runtime mode production --- .../src/views/templates/edit.controller.js | 9 +++++---- .../src/views/templates/edit.html | 16 +++++++--------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index e231ff51f5..1e14cf1e87 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -83,9 +83,7 @@ vm.save = function (suppressNotification) { vm.page.saveButtonState = "busy"; - if (vm.editor) { - vm.template.content = vm.editor.getValue(); - } + vm.template.content = vm.editor.getValue(); contentEditingHelper.contentEditorPerformSave({ saveMethod: templateResource.save, @@ -228,7 +226,10 @@ onLoad: function (_editor) { vm.editor = _editor; - //Update the auto-complete method to use ctrl+alt+space + // Set read-only when using runtime mode Production + _editor.setReadOnly(vm.runtimeModeProduction); + + // Update the auto-complete method to use ctrl+alt+space _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); // Unassigns the keybinding (That was previously auto-complete) diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index 663e0a1f04..5fb4118491 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -23,15 +23,13 @@ - -
- Template content is not editable when using runtime mode Production. -
-
+ + +
+ Template content is not editable when using runtime mode Production. +
- - -
+
@@ -99,7 +97,7 @@
From 234552bcab93e7b701d0083933a91527498237a8 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 10:34:11 +0200 Subject: [PATCH 085/101] Disable editing partial views and macro files --- .../partialViewMacros/edit.controller.js | 683 +++++++-------- .../src/views/partialViewMacros/edit.html | 118 +-- .../src/views/partialViews/edit.controller.js | 819 +++++++++--------- .../src/views/partialViews/edit.html | 149 ++-- .../src/views/templates/edit.html | 242 +++--- 5 files changed, 1007 insertions(+), 1004 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js index 2f7d879607..d72cdbe69a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.controller.js @@ -1,362 +1,367 @@ (function () { - "use strict"; + "use strict"; - function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource, editorService) { + function partialViewMacrosEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, macroResource, editorService) { - var vm = this; + var vm = this; - vm.header = {}; - vm.header.editorfor = "visuallyHiddenTexts_newPartialViewMacro"; - vm.header.setPageTitle = true; - vm.page = {}; - vm.page.loading = true; - vm.partialViewMacroFile = {}; + vm.runtimeModeProduction = Umbraco.Sys.ServerVariables.application.runtimeMode == 'Production'; - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; + vm.header = {}; + vm.header.editorfor = "visuallyHiddenTexts_newPartialViewMacro"; + vm.header.setPageTitle = true; + vm.page = {}; + vm.page.loading = true; + vm.partialViewMacroFile = {}; - // insert buttons - vm.page.insertDefaultButton = { - labelKey: "general_insert", - addEllipsis: "true", - handler: function() { - vm.openInsertOverlay(); - } - }; - vm.page.insertSubButtons = [ - { - labelKey: "template_insertPageField", - addEllipsis: "true", - handler: function () { - vm.openPageFieldOverlay(); - } - }, - { - labelKey: "template_insertMacro", - addEllipsis: "true", - handler: function () { - vm.openMacroOverlay() - } - }, - { - labelKey: "template_insertDictionaryItem", - addEllipsis: "true", - handler: function () { - vm.openDictionaryItemOverlay(); - } - } - ]; + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; - // bind functions to view model - vm.save = save; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; + // insert buttons + vm.page.insertDefaultButton = { + labelKey: "general_insert", + addEllipsis: "true", + handler: function () { + vm.openInsertOverlay(); + } + }; + vm.page.insertSubButtons = [ + { + labelKey: "template_insertPageField", + addEllipsis: "true", + handler: function () { + vm.openPageFieldOverlay(); + } + }, + { + labelKey: "template_insertMacro", + addEllipsis: "true", + handler: function () { + vm.openMacroOverlay() + } + }, + { + labelKey: "template_insertDictionaryItem", + addEllipsis: "true", + handler: function () { + vm.openDictionaryItemOverlay(); + } + } + ]; - /* Functions bound to view model */ + // bind functions to view model + vm.save = save; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; - function save() { + /* Functions bound to view model */ - vm.page.saveButtonState = "busy"; - vm.partialViewMacro.content = vm.editor.getValue(); + function save() { - contentEditingHelper.contentEditorPerformSave({ - saveMethod: codefileResource.save, - scope: $scope, - content: vm.partialViewMacro, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - // create macro if needed - if($routeParams.create && $routeParams.nomacro !== "true") { - macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function (created) { - navigationService.syncTree({ - tree: "macros", - path: '-1,new', - forceReload: true, - activate: false - }); - completeSave(saved); - }, Utilities.noop); - - - } else { - completeSave(saved); - } - - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); + vm.page.saveButtonState = "busy"; + vm.partialViewMacro.content = vm.editor.getValue(); + contentEditingHelper.contentEditorPerformSave({ + saveMethod: codefileResource.save, + scope: $scope, + content: vm.partialViewMacro, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + // create macro if needed + if ($routeParams.create && $routeParams.nomacro !== "true") { + macroResource.createPartialViewMacroWithFile(saved.virtualPath, saved.name).then(function (created) { + navigationService.syncTree({ + tree: "macros", + path: '-1,new', + forceReload: true, + activate: false }); + completeSave(saved); + }, Utilities.noop); + + } else { + completeSave(saved); } - function completeSave(saved) { + }, function (err) { - localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { - notificationsService.success(headerValue, msgValue); - }); - }); + vm.page.saveButtonState = "error"; - //check if the name changed, if so we need to redirect - if (vm.partialViewMacro.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); - } - else { - vm.page.saveButtonState = "success"; - vm.partialViewMacro = saved; + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); - //sync state - editorState.set(vm.partialViewMacro); - - // normal tree sync - navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - - // clear $dirty state on form - setFormState("pristine"); - } - - } - - function openInsertOverlay() { - var insertOverlay = { - allowedTypes: { - macro: true, - dictionary: true, - umbracoField: true - }, - submit: function(model) { - switch(model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - editorService.close(); - }, - close: function(oldModel) { - // close the dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.insertCodeSnippet(insertOverlay); - } - - function openMacroOverlay() { - var macroPicker = { - dialogData: {}, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - editorService.close(); - }, - close: function() { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.macroPicker(macroPicker); - } - - - function openPageFieldOverlay() { - var insertFieldEditor = { - submit: function (model) { - insert(model.umbracoField); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.insertField(insertFieldEditor); - } - - - function openDictionaryItemOverlay() { - - var labelKeys = [ - "template_insertDictionaryItem", - "emptyStates_emptyDictionaryTree" - ]; - - localizationService.localizeMany(labelKeys).then(function(values){ - var title = values[0]; - var emptyStateMessage = values[1]; - - var dictionaryPicker = { - section: "translation", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: title, - emptyStateMessage: emptyStateMessage, - select: function(node){ - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - - editorService.treePicker(dictionaryPicker); - - }); - } - - function openQueryBuilderOverlay() { - var queryBuilder = { - submit: function (model) { - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.queryBuilder(queryBuilder); - } - - /* Local functions */ - - function init() { - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); - - if ($routeParams.create) { - - var snippet = "Empty"; - - if($routeParams.snippet) { - snippet = $routeParams.snippet; - } - - codefileResource.getScaffold("partialViewMacros", $routeParams.id, snippet).then(function (partialViewMacro) { - if ($routeParams.name) { - partialViewMacro.name = $routeParams.name; - } - ready(partialViewMacro, false); - }); - - } else { - codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { - ready(partialViewMacro, true); - }); - } - } - - function ready(partialViewMacro, syncTree) { - - vm.page.loading = false; - vm.partialViewMacro = partialViewMacro; - - //sync state - editorState.set(vm.partialViewMacro); - - if (syncTree) { - navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px' - }, - onLoad: function(_editor) { - vm.editor = _editor; - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!$routeParams.create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - - } - } - - } - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); - } - } - - - init(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.EditController", partialViewMacrosEditController); + function completeSave(saved) { + + localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { + notificationsService.success(headerValue, msgValue); + }); + }); + + //check if the name changed, if so we need to redirect + if (vm.partialViewMacro.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); + } + else { + vm.page.saveButtonState = "success"; + vm.partialViewMacro = saved; + + //sync state + editorState.set(vm.partialViewMacro); + + // normal tree sync + navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + + // clear $dirty state on form + setFormState("pristine"); + } + + } + + function openInsertOverlay() { + var insertOverlay = { + allowedTypes: { + macro: true, + dictionary: true, + umbracoField: true + }, + submit: function (model) { + switch (model.insert.type) { + case "macro": + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + case "dictionary": + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + editorService.close(); + }, + close: function (oldModel) { + // close the dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.insertCodeSnippet(insertOverlay); + } + + function openMacroOverlay() { + var macroPicker = { + dialogData: {}, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.macroPicker(macroPicker); + } + + + function openPageFieldOverlay() { + var insertFieldEditor = { + submit: function (model) { + insert(model.umbracoField); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.insertField(insertFieldEditor); + } + + + function openDictionaryItemOverlay() { + + var labelKeys = [ + "template_insertDictionaryItem", + "emptyStates_emptyDictionaryTree" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + var title = values[0]; + var emptyStateMessage = values[1]; + + var dictionaryPicker = { + section: "translation", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: title, + emptyStateMessage: emptyStateMessage, + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + + editorService.treePicker(dictionaryPicker); + + }); + } + + function openQueryBuilderOverlay() { + var queryBuilder = { + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.queryBuilder(queryBuilder); + } + + /* Local functions */ + + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + if ($routeParams.create) { + + var snippet = "Empty"; + + if ($routeParams.snippet) { + snippet = $routeParams.snippet; + } + + codefileResource.getScaffold("partialViewMacros", $routeParams.id, snippet).then(function (partialViewMacro) { + if ($routeParams.name) { + partialViewMacro.name = $routeParams.name; + } + ready(partialViewMacro, false); + }); + + } else { + codefileResource.getByPath('partialViewMacros', $routeParams.id).then(function (partialViewMacro) { + ready(partialViewMacro, true); + }); + } + } + + function ready(partialViewMacro, syncTree) { + + vm.page.loading = false; + vm.partialViewMacro = partialViewMacro; + + //sync state + editorState.set(vm.partialViewMacro); + + if (syncTree) { + navigationService.syncTree({ tree: "partialViewMacros", path: vm.partialViewMacro.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px' + }, + onLoad: function (_editor) { + vm.editor = _editor; + + // Set read-only when using runtime mode Production + _editor.setReadOnly(vm.runtimeModeProduction); + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!$routeParams.create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + //change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + vm.editor.on("change", changeAceEditor); + + } + } + + } + + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if (state === "dirty") { + currentForm.$setDirty(); + } else if (state === "pristine") { + currentForm.$setPristine(); + } + } + + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PartialViewMacros.EditController", partialViewMacrosEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html index 36fff3a044..d15bd866eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html @@ -1,81 +1,81 @@
- + -
+ - + - - + + - - - + + + +
+ Template content is not editable when using runtime mode Production. +
-
+
-
+
- - + + - - + + -
+
-
+
-
-
+
+
-
-
-
+
+
+
- + - + - - + + - + - + -
- +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js index 1e21a63755..512c1176c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.controller.js @@ -1,433 +1,438 @@ (function () { - "use strict"; + "use strict"; - function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, editorService) { + function PartialViewsEditController($scope, $routeParams, codefileResource, assetsService, notificationsService, editorState, navigationService, appState, macroService, angularHelper, $timeout, contentEditingHelper, localizationService, templateHelper, editorService) { - var vm = this; - var infiniteMode = $scope.model && $scope.model.infiniteMode; - var id = infiniteMode ? $scope.model.id : $routeParams.id; - var create = infiniteMode ? $scope.model.create : $routeParams.create; - var snippet = infiniteMode ? $scope.model.snippet : $routeParams.snippet; + var vm = this; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var id = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; + var snippet = infiniteMode ? $scope.model.snippet : $routeParams.snippet; - function close() { - if ($scope.model.close) { - $scope.model.close($scope.model); - } + function close() { + if ($scope.model.close) { + $scope.model.close($scope.model); + } + } + + vm.close = close; + + vm.runtimeModeProduction = Umbraco.Sys.ServerVariables.application.runtimeMode == 'Production'; + + vm.header = {}; + vm.header.editorfor = "visuallyHiddenTexts_newPartialView"; + vm.header.setPageTitle = true; + + vm.page = {}; + vm.page.loading = true; + vm.partialView = {}; + + //menu + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + + // insert buttons + vm.page.insertDefaultButton = { + labelKey: "general_insert", + addEllipsis: "true", + handler: function () { + vm.openInsertOverlay(); + } + }; + vm.page.insertSubButtons = [ + { + labelKey: "template_insertPageField", + addEllipsis: "true", + handler: function () { + vm.openPageFieldOverlay(); } + }, + { + labelKey: "template_insertMacro", + addEllipsis: "true", + handler: function () { + vm.openMacroOverlay() + } + }, + { + labelKey: "template_insertDictionaryItem", + addEllipsis: "true", + handler: function () { + vm.openDictionaryItemOverlay(); + } + } + ]; - vm.close = close; + //Used to toggle the keyboard shortcut modal + //From a custom keybinding in ace editor - that conflicts with our own to show the dialog + vm.showKeyboardShortcut = false; - vm.header = {}; - vm.header.editorfor = "visuallyHiddenTexts_newPartialView"; - vm.header.setPageTitle = true; + //Keyboard shortcuts for help dialog + vm.page.keyboardShortcutsOverview = []; - vm.page = {}; - vm.page.loading = true; - vm.partialView = {}; + templateHelper.getGeneralShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); + templateHelper.getPartialViewEditorShortcuts().then(function (data) { + vm.page.keyboardShortcutsOverview.push(data); + }); - //menu - vm.page.menu = {}; - vm.page.menu.currentSection = appState.getSectionState("currentSection"); - vm.page.menu.currentNode = null; - // insert buttons - vm.page.insertDefaultButton = { - labelKey: "general_insert", - addEllipsis: "true", - handler: function() { - vm.openInsertOverlay(); - } - }; - vm.page.insertSubButtons = [ - { - labelKey: "template_insertPageField", - addEllipsis: "true", - handler: function () { - vm.openPageFieldOverlay(); - } - }, - { - labelKey: "template_insertMacro", - addEllipsis: "true", - handler: function () { - vm.openMacroOverlay() - } - }, - { - labelKey: "template_insertDictionaryItem", - addEllipsis: "true", - handler: function () { - vm.openDictionaryItemOverlay(); - } - } - ]; + // bind functions to view model + vm.save = save; + vm.openPageFieldOverlay = openPageFieldOverlay; + vm.openDictionaryItemOverlay = openDictionaryItemOverlay; + vm.openQueryBuilderOverlay = openQueryBuilderOverlay; + vm.openMacroOverlay = openMacroOverlay; + vm.openInsertOverlay = openInsertOverlay; - //Used to toggle the keyboard shortcut modal - //From a custom keybinding in ace editor - that conflicts with our own to show the dialog - vm.showKeyboardShortcut = false; + /* Functions bound to view model */ - //Keyboard shortcuts for help dialog - vm.page.keyboardShortcutsOverview = []; + function save() { - templateHelper.getGeneralShortcuts().then(function(data){ - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getEditorShortcuts().then(function(data){ - vm.page.keyboardShortcutsOverview.push(data); - }); - templateHelper.getPartialViewEditorShortcuts().then(function(data){ - vm.page.keyboardShortcutsOverview.push(data); + vm.page.saveButtonState = "busy"; + vm.partialView.content = vm.editor.getValue(); + + contentEditingHelper.contentEditorPerformSave({ + saveMethod: codefileResource.save, + scope: $scope, + content: vm.partialView, + rebindCallback: function (orignal, saved) { } + }).then(function (saved) { + + localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_partialViewSavedText").then(function (msgValue) { + notificationsService.success(headerValue, msgValue); + }); }); - - // bind functions to view model - vm.save = save; - vm.openPageFieldOverlay = openPageFieldOverlay; - vm.openDictionaryItemOverlay = openDictionaryItemOverlay; - vm.openQueryBuilderOverlay = openQueryBuilderOverlay; - vm.openMacroOverlay = openMacroOverlay; - vm.openInsertOverlay = openInsertOverlay; - - /* Functions bound to view model */ - - function save() { - - vm.page.saveButtonState = "busy"; - vm.partialView.content = vm.editor.getValue(); - - contentEditingHelper.contentEditorPerformSave({ - saveMethod: codefileResource.save, - scope: $scope, - content: vm.partialView, - rebindCallback: function (orignal, saved) {} - }).then(function (saved) { - - localizationService.localize("speechBubbles_partialViewSavedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_partialViewSavedText").then(function(msgValue) { - notificationsService.success(headerValue, msgValue); - }); - }); - - //check if the name changed, if so we need to redirect - if (vm.partialView.id !== saved.id) { - contentEditingHelper.redirectToRenamedContent(saved.id); - } - else { - vm.page.saveButtonState = "success"; - vm.partialView = saved; - - //sync state - editorState.set(vm.partialView); - - // normal tree sync - navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - - // clear $dirty state on form - setFormState("pristine"); - } - }, function (err) { - - vm.page.saveButtonState = "error"; - - localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { - localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { - notificationsService.error(headerValue, msgValue); - }); - }); - - }); - + //check if the name changed, if so we need to redirect + if (vm.partialView.id !== saved.id) { + contentEditingHelper.redirectToRenamedContent(saved.id); } + else { + vm.page.saveButtonState = "success"; + vm.partialView = saved; - function openInsertOverlay() { - var insertOverlay = { - allowedTypes: { - macro: true, - dictionary: true, - umbracoField: true - }, - submit: function(model) { + //sync state + editorState.set(vm.partialView); - switch(model.insert.type) { - case "macro": - var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); - insert(macroObject.syntax); - break; - case "dictionary": - var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); - insert(code); - break; - case "umbracoField": - insert(model.insert.umbracoField); - break; - } - editorService.close(); - }, - close: function() { - // close the dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.insertCodeSnippet(insertOverlay); + // normal tree sync + navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + + // clear $dirty state on form + setFormState("pristine"); } + }, function (err) { + vm.page.saveButtonState = "error"; - function openMacroOverlay() { - var macroPicker = { - dialogData: {}, - submit: function (model) { - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); - insert(macroObject.syntax); - editorService.close(); - }, - close: function() { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.macroPicker(macroPicker); - } + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); - function openPageFieldOverlay() { - var insertFieldEditor = { - submit: function (model) { - insert(model.umbracoField); - editorService.close(); - }, - close: function () { - editorService.close(); - vm.editor.focus(); - } - }; - editorService.insertField(insertFieldEditor); - } - - - function openDictionaryItemOverlay() { - - var labelKeys = [ - "template_insertDictionaryItem", - "emptyStates_emptyDictionaryTree" - ]; - - localizationService.localizeMany(labelKeys).then(function(values){ - var title = values[0]; - var emptyStateMessage = values[1]; - - var dictionaryItem = { - section: "translation", - treeAlias: "dictionary", - entityType: "dictionary", - multiPicker: false, - title: title, - emptyStateMessage: emptyStateMessage, - select: function(node){ - var code = templateHelper.getInsertDictionarySnippet(node.name); - insert(code); - editorService.close(); - }, - close: function (model) { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.treePicker(dictionaryItem); - }); - } - - function openQueryBuilderOverlay() { - var queryBuilder = { - title: "Query for content", - submit: function (model) { - var code = templateHelper.getQuerySnippet(model.result.queryExpression); - insert(code); - editorService.close(); - }, - close: function () { - // close dialog - editorService.close(); - // focus editor - vm.editor.focus(); - } - }; - editorService.queryBuilder(queryBuilder); - } - - /* Local functions */ - - function init() { - //we need to load this somewhere, for now its here. - assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); - - if (create) { - - if (!snippet) { - snippet = "Empty"; - } - - codefileResource.getScaffold("partialViews", id, snippet).then(function (partialView) { - ready(partialView, false); - }); - - } else { - codefileResource.getByPath('partialViews', id).then(function (partialView) { - ready(partialView, true); - }); - } - - } - - function ready(partialView, syncTree) { - - vm.page.loading = false; - vm.partialView = partialView; - - //sync state - editorState.set(vm.partialView); - - if (!infiniteMode && syncTree) { - navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { - vm.page.menu.currentNode = syncArgs.node; - }); - } - - // ace configuration - vm.aceOption = { - mode: "razor", - theme: "chrome", - showPrintMargin: false, - advanced: { - fontSize: '14px' - }, - onLoad: function(_editor) { - vm.editor = _editor; - - //Update the auto-complete method to use ctrl+alt+space - _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); - - //Unassigns the keybinding (That was previously auto-complete) - //As conflicts with our own tree search shortcut - _editor.commands.bindKey("ctrl-space", null); - - // Assign new keybinding - _editor.commands.addCommands([ - //Disable (alt+shift+K) - //Conflicts with our own show shortcuts dialog - this overrides it - { - name: 'unSelectOrFindPrevious', - bindKey: 'Alt-Shift-K', - exec: function () { - //Toggle the show keyboard shortcuts overlay - $scope.$apply(function () { - vm.showKeyboardShortcut = !vm.showKeyboardShortcut; - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoValue', - bindKey: 'Alt-Shift-V', - exec: function () { - $scope.$apply(function () { - openPageFieldOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertDictionary', - bindKey: 'Alt-Shift-D', - exec: function () { - $scope.$apply(function () { - openDictionaryItemOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertUmbracoMacro', - bindKey: 'Alt-Shift-M', - exec: function () { - $scope.$apply(function () { - openMacroOverlay(); - }); - }, - readOnly: true - }, - { - name: 'insertQuery', - bindKey: 'Alt-Shift-Q', - exec: function () { - $scope.$apply(function () { - openQueryBuilderOverlay(); - }); - }, - readOnly: true - } - - ]); - - // initial cursor placement - // Keep cursor in name field if we are create a new template - // else set the cursor at the bottom of the code editor - if(!create) { - $timeout(function(){ - vm.editor.navigateFileEnd(); - vm.editor.focus(); - persistCurrentLocation(); - }); - } - - //change on blur, focus - vm.editor.on("blur", persistCurrentLocation); - vm.editor.on("focus", persistCurrentLocation); - vm.editor.on("change", changeAceEditor); - - } - } - - } - - function insert(str) { - vm.editor.focus(); - vm.editor.moveCursorToPosition(vm.currentPosition); - vm.editor.insert(str); - - // set form state to $dirty - setFormState("dirty"); - } - - function persistCurrentLocation() { - vm.currentPosition = vm.editor.getCursorPosition(); - } - - function changeAceEditor() { - setFormState("dirty"); - } - - function setFormState(state) { - - // get the current form - var currentForm = angularHelper.getCurrentForm($scope); - - // set state - if(state === "dirty") { - currentForm.$setDirty(); - } else if(state === "pristine") { - currentForm.$setPristine(); - } - } - - - init(); + }); } - angular.module("umbraco").controller("Umbraco.Editors.PartialViews.EditController", PartialViewsEditController); + function openInsertOverlay() { + var insertOverlay = { + allowedTypes: { + macro: true, + dictionary: true, + umbracoField: true + }, + submit: function (model) { + + switch (model.insert.type) { + case "macro": + var macroObject = macroService.collectValueData(model.insert.selectedMacro, model.insert.macroParams, "Mvc"); + insert(macroObject.syntax); + break; + case "dictionary": + var code = templateHelper.getInsertDictionarySnippet(model.insert.node.name); + insert(code); + break; + case "umbracoField": + insert(model.insert.umbracoField); + break; + } + editorService.close(); + }, + close: function () { + // close the dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.insertCodeSnippet(insertOverlay); + } + + + function openMacroOverlay() { + var macroPicker = { + dialogData: {}, + submit: function (model) { + var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, "Mvc"); + insert(macroObject.syntax); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.macroPicker(macroPicker); + } + + function openPageFieldOverlay() { + var insertFieldEditor = { + submit: function (model) { + insert(model.umbracoField); + editorService.close(); + }, + close: function () { + editorService.close(); + vm.editor.focus(); + } + }; + editorService.insertField(insertFieldEditor); + } + + + function openDictionaryItemOverlay() { + + var labelKeys = [ + "template_insertDictionaryItem", + "emptyStates_emptyDictionaryTree" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + var title = values[0]; + var emptyStateMessage = values[1]; + + var dictionaryItem = { + section: "translation", + treeAlias: "dictionary", + entityType: "dictionary", + multiPicker: false, + title: title, + emptyStateMessage: emptyStateMessage, + select: function (node) { + var code = templateHelper.getInsertDictionarySnippet(node.name); + insert(code); + editorService.close(); + }, + close: function (model) { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.treePicker(dictionaryItem); + }); + } + + function openQueryBuilderOverlay() { + var queryBuilder = { + title: "Query for content", + submit: function (model) { + var code = templateHelper.getQuerySnippet(model.result.queryExpression); + insert(code); + editorService.close(); + }, + close: function () { + // close dialog + editorService.close(); + // focus editor + vm.editor.focus(); + } + }; + editorService.queryBuilder(queryBuilder); + } + + /* Local functions */ + + function init() { + //we need to load this somewhere, for now its here. + assetsService.loadCss("lib/ace-razor-mode/theme/razor_chrome.css", $scope); + + if (create) { + + if (!snippet) { + snippet = "Empty"; + } + + codefileResource.getScaffold("partialViews", id, snippet).then(function (partialView) { + ready(partialView, false); + }); + + } else { + codefileResource.getByPath('partialViews', id).then(function (partialView) { + ready(partialView, true); + }); + } + + } + + function ready(partialView, syncTree) { + + vm.page.loading = false; + vm.partialView = partialView; + + //sync state + editorState.set(vm.partialView); + + if (!infiniteMode && syncTree) { + navigationService.syncTree({ tree: "partialViews", path: vm.partialView.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + // ace configuration + vm.aceOption = { + mode: "razor", + theme: "chrome", + showPrintMargin: false, + advanced: { + fontSize: '14px' + }, + onLoad: function (_editor) { + vm.editor = _editor; + + // Set read-only when using runtime mode Production + _editor.setReadOnly(vm.runtimeModeProduction); + + //Update the auto-complete method to use ctrl+alt+space + _editor.commands.bindKey("ctrl-alt-space", "startAutocomplete"); + + //Unassigns the keybinding (That was previously auto-complete) + //As conflicts with our own tree search shortcut + _editor.commands.bindKey("ctrl-space", null); + + // Assign new keybinding + _editor.commands.addCommands([ + //Disable (alt+shift+K) + //Conflicts with our own show shortcuts dialog - this overrides it + { + name: 'unSelectOrFindPrevious', + bindKey: 'Alt-Shift-K', + exec: function () { + //Toggle the show keyboard shortcuts overlay + $scope.$apply(function () { + vm.showKeyboardShortcut = !vm.showKeyboardShortcut; + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoValue', + bindKey: 'Alt-Shift-V', + exec: function () { + $scope.$apply(function () { + openPageFieldOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertDictionary', + bindKey: 'Alt-Shift-D', + exec: function () { + $scope.$apply(function () { + openDictionaryItemOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertUmbracoMacro', + bindKey: 'Alt-Shift-M', + exec: function () { + $scope.$apply(function () { + openMacroOverlay(); + }); + }, + readOnly: true + }, + { + name: 'insertQuery', + bindKey: 'Alt-Shift-Q', + exec: function () { + $scope.$apply(function () { + openQueryBuilderOverlay(); + }); + }, + readOnly: true + } + + ]); + + // initial cursor placement + // Keep cursor in name field if we are create a new template + // else set the cursor at the bottom of the code editor + if (!create) { + $timeout(function () { + vm.editor.navigateFileEnd(); + vm.editor.focus(); + persistCurrentLocation(); + }); + } + + //change on blur, focus + vm.editor.on("blur", persistCurrentLocation); + vm.editor.on("focus", persistCurrentLocation); + vm.editor.on("change", changeAceEditor); + + } + } + + } + + function insert(str) { + vm.editor.focus(); + vm.editor.moveCursorToPosition(vm.currentPosition); + vm.editor.insert(str); + + // set form state to $dirty + setFormState("dirty"); + } + + function persistCurrentLocation() { + vm.currentPosition = vm.editor.getCursorPosition(); + } + + function changeAceEditor() { + setFormState("dirty"); + } + + function setFormState(state) { + + // get the current form + var currentForm = angularHelper.getCurrentForm($scope); + + // set state + if (state === "dirty") { + currentForm.$setDirty(); + } else if (state === "pristine") { + currentForm.$setPristine(); + } + } + + + init(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.PartialViews.EditController", PartialViewsEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html index 4a4d01385e..ec090e6071 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html @@ -1,97 +1,98 @@
- + -
+ - + - - + + - + - - + + +
+ Template content is not editable when using runtime mode Production. +
-
+
-
+
- - + + - - + + -
+
-
+
-
-
+
+
-
-
- -
+ + - +
- - - - + - + + + + - - - - - - + - + + -
- + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index 5fb4118491..dd2cda9a54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -1,150 +1,142 @@
- + -
+ - + - - + + - - - - -
- Template content is not editable when using runtime mode Production. -
+ + -
+ +
+ Template content is not editable when using runtime mode Production. +
-
+
-
+
- +
- + -
+ -
+
-
+
- - +
- - + + - - - -
- -
- -
-
- - - - - - - - - - - - - - - - - + - + - +
- +
-
- +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
From fabb49feaf2cfffd4f3d8ee0cce4a534b332bfdb Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 10:45:29 +0200 Subject: [PATCH 086/101] Remove template from warning message --- src/Umbraco.Core/EmbeddedResources/Lang/en.xml | 2 +- src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml | 2 +- src/Umbraco.Core/EmbeddedResources/Lang/nl.xml | 2 +- src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html | 2 +- src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html | 2 +- src/Umbraco.Web.UI.Client/src/views/templates/edit.html | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 49ccf24dd7..d75e70bb07 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -1630,7 +1630,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Editor - Production.]]> + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 04883067f6..9641fda47f 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -1683,7 +1683,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Rich Text Editor - Production.]]> + Production.]]> Failed to delete template with ID %0% Edit template Sections diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml index 06762e1dd7..1d76ac06f9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml @@ -1444,7 +1444,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Rich Text Editor - Production.]]> + Production.]]> Kan sjabloon met ID %0% niet verwijderen Sjabloon aanpassen Secties diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html index d15bd866eb..7cffa70aa1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/edit.html @@ -23,7 +23,7 @@
- Template content is not editable when using runtime mode Production. + Content is not editable when using runtime mode Production.
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html index ec090e6071..e1eb195545 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/edit.html @@ -24,7 +24,7 @@
- Template content is not editable when using runtime mode Production. + Content is not editable when using runtime mode Production.
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index dd2cda9a54..a5527095d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -25,7 +25,7 @@
- Template content is not editable when using runtime mode Production. + Content is not editable when using runtime mode Production.
From d2806501d993bc34156aa6918089bfc2e6bdece8 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 11:07:24 +0200 Subject: [PATCH 087/101] Fix integration test ACE Editor mock --- .../test/unit/app/templates/template-editor-controller.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js index b4615eaad8..9b872f137f 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/templates/template-editor-controller.spec.js @@ -1,4 +1,4 @@ -(function() { +(function() { "use strict"; describe("templates editor controller", @@ -26,6 +26,7 @@ getCursorPosition: function() {}, getValue: function() {}, setValue: function() {}, + setReadOnly: function () { }, focus: function() {}, clearSelection: function() {}, navigateFileStart: function() {}, From 2d217d505d3df54033eb279cbb677840f543fc25 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 14:17:45 +0200 Subject: [PATCH 088/101] Fix integration tests --- .../Persistence/Repositories/ContentTypeRepositoryTest.cs | 5 ++++- .../Persistence/Repositories/DocumentRepositoryTest.cs | 5 ++++- .../Persistence/Repositories/TemplateRepositoryTest.cs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index e2a691a102..aae69e2f61 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -79,6 +79,9 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest var provider = ScopeProvider; using (var scope = provider.CreateScope()) { + var runtimeSettingsMock = new Mock>(); + runtimeSettingsMock.Setup(x => x.CurrentValue).Returns(new RuntimeSettings()); + var templateRepo = new TemplateRepository( (IScopeAccessor)provider, AppCaches.Disabled, @@ -87,7 +90,7 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest IOHelper, ShortStringHelper, Mock.Of(), - Mock.Of>()); + runtimeSettingsMock.Object); var repository = ContentTypeRepository; Template[] templates = { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index 319470917e..9870926544 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -111,7 +111,10 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest { appCaches ??= AppCaches; - templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of(), Mock.Of>()); + var runtimeSettingsMock = new Mock>(); + runtimeSettingsMock.Setup(x => x.CurrentValue).Returns(new RuntimeSettings()); + + templateRepository = new TemplateRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, Mock.Of(), runtimeSettingsMock.Object); var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs index 961c11f471..e0665aaf6d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/TemplateRepositoryTest.cs @@ -57,10 +57,13 @@ public class TemplateRepositoryTest : UmbracoIntegrationTest private IHostingEnvironment HostingEnvironment => GetRequiredService(); private FileSystems FileSystems => GetRequiredService(); + private IViewHelper ViewHelper => GetRequiredService(); + private IOptionsMonitor RuntimeSettings => GetRequiredService>(); + private ITemplateRepository CreateRepository(IScopeProvider provider) => - new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, Mock.Of>()); + new TemplateRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), FileSystems, IOHelper, ShortStringHelper, ViewHelper, RuntimeSettings); [Test] public void Can_Instantiate_Repository() From 08a7a26743576ba585380f19bcd202ff4d2301f1 Mon Sep 17 00:00:00 2001 From: Matt Brailsford Date: Wed, 13 Jul 2022 23:08:44 +0200 Subject: [PATCH 089/101] Allow dashboards to also be the "last accessed item" (#12689) * Allow dashboards to also be the "last accessed item" * Use tripple = --- .../src/common/services/history.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/history.service.js b/src/Umbraco.Web.UI.Client/src/common/services/history.service.js index 114d002a4d..d94ff52183 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/history.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/history.service.js @@ -130,7 +130,7 @@ angular.module('umbraco.services') getLastAccessedItemForSection: function (sectionAlias) { for (var i = 0, len = nArray.length; i < len; i++) { var item = nArray[i]; - if (item.link.indexOf(sectionAlias + "/") === 0) { + if (item.link === sectionAlias || item.link.indexOf(sectionAlias + "/") === 0) { return item; } } @@ -138,4 +138,4 @@ angular.module('umbraco.services') return null; } }; -}); \ No newline at end of file +}); From 338bc2c19c2feae9719d3b9da9497da4d8e18a73 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 20 Jul 2022 09:36:26 +0200 Subject: [PATCH 090/101] Bumb version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 11587874da..f74c6fceb1 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.1.0-rc", + "version": "10.1.0-rc2", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 197ec2ab1ba80673d143ce07bc1c44221ad90912 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 25 Jul 2022 11:33:52 +0200 Subject: [PATCH 091/101] fix: restore quickinstall view in the installer with options to customize the database or skip entirely --- .../src/installer/steps/user.html | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index 9049512c8c..f0cb8ec1db 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -66,16 +66,30 @@
+
+
+ +
+
+ Provider: {{installer.current.model.quickInstallSettings.displayName}} +
Name: {{installer.current.model.quickInstallSettings.defaultDatabaseName}}
+
+
+
+
- - +
+ + +
-
From 3baae5ab535f9503c02353dced5ffe864c59286a Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 25 Jul 2022 18:02:40 +0200 Subject: [PATCH 092/101] Check if content has an identity before attempting to get it's ancestors (#12733) * Check if content has an identity before attempting to get it's ancestors * Check if we even need to go to the parent * Fail earlier * Update src/Umbraco.Web.BackOffice/Controllers/ContentController.cs Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- src/Umbraco.Web.BackOffice/Controllers/ContentController.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 0d45185eb9..1ca12f2f42 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -1797,6 +1797,11 @@ public class ContentController : ContentControllerBase private IEnumerable GetPublishedCulturesFromAncestors(IContent? content) { + if (content?.ParentId is not -1 && content?.HasIdentity is false) + { + content = _contentService.GetById(content.ParentId); + } + if (content?.ParentId == -1) { return content.PublishedCultures; From abee27a99f139864e0491eed9b32c1276911d334 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 25 Jul 2022 15:42:46 +0200 Subject: [PATCH 093/101] fix: ensure the input field to edit tabs is only shown when active This fix prevents Firefox (and possibly others) from preventing the click event, which now bubbles up as it should. --- .../views/components/contenttype/umb-content-type-tab.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html index b62e3f17d9..a145955334 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/contenttype/umb-content-type-tab.html @@ -28,9 +28,9 @@ ng-if="vm.tab.inherited"> -
{{ vm.tab.name }}
+
{{ vm.tab.name }}
Date: Wed, 27 Jul 2022 09:31:38 +0200 Subject: [PATCH 094/101] change "All sections" to "No sections" (#12708) --- .../src/views/components/users/umb-user-group-preview.html | 2 +- .../src/views/users/views/groups/groups.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html index db0c6c86c3..f96cdfa0ee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-group-preview.html @@ -9,7 +9,7 @@ Sections: {{ section.name }} - All sections + No sections
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index bc33b3b577..0d77e3e97e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -96,7 +96,7 @@
{{ section.name }}, - All sections + No sections
From 6ce9ea4950b8a053e49bd3adec7bedb8d6a2d4a5 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 2 Aug 2022 11:00:58 +0200 Subject: [PATCH 095/101] Force the allowed avatar image types. We do not want to use the Umbraco:Cms:Content:Imaging:ImageFileTypes as this could very well be different for content. --- src/Umbraco.Web.BackOffice/Controllers/UsersController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 24e5a77a23..f734d8626b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -179,8 +179,9 @@ public class UsersController : BackOfficeNotificationsController var fileName = file.FileName.Trim(new[] { '\"' }).TrimEnd(); var safeFileName = fileName.ToSafeFileName(shortStringHelper); var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + const string allowedAvatarFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; - if (contentSettings.DisallowedUploadFiles.Contains(ext) == false) + if (allowedAvatarFileTypes.Contains(ext) == true && contentSettings.DisallowedUploadFiles.Contains(ext) == false) { //generate a path of known data, we don't want this path to be guessable user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash() + "." + ext; From 30210f5de51ad3c5bca552d692101c05e859fba2 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 2 Aug 2022 12:02:46 +0200 Subject: [PATCH 096/101] Enable package validation --- src/Directory.Build.props | 8 ++++++++ src/JsonSchema/JsonSchema.csproj | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b7d39cc6f5..ce54e08edd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -42,4 +42,12 @@ true + + + + true + 10.0.0 + true + true + diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 5ab8db1d99..ea0ce9b7c3 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -4,6 +4,7 @@ net6.0 true false + false @@ -34,5 +35,4 @@ - diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 5f067d8a9d..cf86519c8e 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2,6 +2,7 @@ net6.0 Umbraco.Cms.Web.UI + false From 75a4044f12b0a4603656494dcbcdbf53d92144bb Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 2 Aug 2022 12:32:57 +0200 Subject: [PATCH 097/101] V10: Show more telemetry info on the installer (#12739) * remove absolute-center class from installer and replace with css grid * replace tooltips for telemetry with columns and a direct view of what eact telemetry level means * ensure that inputs will out entire columns and that buttons are aligned as the rest of the system with the CTA on the right side * update buttons on the database screen to match the user screen * set min-width on installer for views with only text content * add border to preconfigured database field * remove extra headline from telemetry data info text * change telemetry info text to 'small' * remove opacity for database settings * update wording of intro text to the installer * add unordered list to the detailed telemetry description * add helper text to indicate that telemetry can be changed later * change wording from 'analytics' to 'telemetry data' * add smooth-steps-tap to noUiSlider for telemetry data * add short text explaining what telemetry is to the installer --- .../umbraco/UmbracoInstall/Index.cshtml | 2 +- .../EmbeddedResources/Lang/en_us.xml | 19 +- .../src/installer/steps/database.html | 310 ++++++++++-------- .../src/installer/steps/user.controller.js | 36 +- .../src/installer/steps/user.html | 224 ++++++++----- .../src/less/installer.less | 71 ++-- .../settings/analytics.controller.js | 1 + .../views/dashboard/settings/analytics.html | 23 +- 8 files changed, 403 insertions(+), 283 deletions(-) diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml index 92e458c3b0..406c311312 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoInstall/Index.cshtml @@ -26,7 +26,7 @@
-
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 9641fda47f..b971e2c13c 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -2528,7 +2528,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Published Status Models Builder Health Check - Analytics + Telemetry data Profiling Getting Started Install Umbraco Forms @@ -2886,8 +2886,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont items returned - Consent for analytics - Analytics level saved! + Consent for telemetry data + Telemetry level saved! - Anonymized site ID, umbraco version, and packages installed. -
- Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use. -
- System information: Webserver, server OS, server framework, server OS language, and database provider. -
- Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode. -
-
We might change what we send on the Detailed level in the future. If so, it will be listed above. +
    +
  • Anonymized site ID, umbraco version, and packages installed.
  • +
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.
  • +
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • +
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.
  • +
+ We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymized information being collected.
]]>
diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html index dc3b972718..e248f3221b 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/database.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/database.html @@ -1,144 +1,188 @@
+

Configure your database

+

+ Enter connection and authentication details for the database you want to + install Umbraco on +

-

Configure your database

-

- Enter connection and authentication details for the database you want to install Umbraco on -

+
+
+ What type of database do you use? + +
+ +
+
- -
- What type of database do you use? - +
+ What is the exact connection string we should use? +
+ +
+ + Enter a valid database connection string. +
+
+ +
+ +
+ +
+
+
+ +
+
+ Where do we find your database? + +
+
+
+ +
+ + Enter server domain or IP +
+
+
+
+ +
+
+
- + + Enter the name of the database
+
+
-
- What is the exact connection string we should use? -
- -
- - Enter a valid database connection string. -
-
- -
- -
- -
-
-
- - - -
-
- Where do we find your database? - -
-
-
- -
- - Enter server domain or IP -
-
-
-
- -
-
- -
- - Enter the name of the database -
-
-
-
- -
-
- What credentials are used to access the database? -
-
- -
- - Enter the database user name -
-
-
- -
-
- -
- - Enter the database password -
-
-
- -
-
- -
-
-
-
-
- - +
-
-
- - - - - - Validating your database connection... - - - - Could not connect to database - -
+ What credentials are used to access the database? +
+
+ +
+ + Enter the database user name +
+
+ +
+
+ +
+ + Enter the database password +
+
+
+ +
+
+ +
+
- +
+
+ +
+
+ + +
+
+ + Validating your database connection... + + + + Could not connect to database + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js index b4aa8eedf5..dab3f1beaf 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js @@ -3,7 +3,7 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f $scope.majorVersion = Umbraco.Sys.ServerVariables.application.version; $scope.passwordPattern = /.*/; $scope.installer.current.model.subscribeToNewsLetter = $scope.installer.current.model.subscribeToNewsLetter || false; - $scope.installer.current.model.telemetryLevel = $scope.installer.current.model.telemetryLevel || $scope.installer.current.model.consentLevels[1].level; + setTelemetryLevelAndDescription($scope.installer.current.model.telemetryIndex ?? 1); if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { var exp = ""; @@ -15,11 +15,6 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f $scope.passwordPattern = new RegExp(exp); } - $scope.consentTooltip = { - show: false, - event: null - }; - if ('noUiSlider' in window) { let consentSliderStartLevel = 2; const initialConsentLevel = $scope.installer.current.model.consentLevels.findIndex(x => x.level === $scope.installer.current.model.telemetryLevel); @@ -36,6 +31,7 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f "min": 1, "max": 3 }, + behaviour: 'smooth-steps-tap', pips: { mode: 'values', density: 50, @@ -63,22 +59,6 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f }); pips.forEach(function (pip) { - pip.addEventListener('mouseenter', function (e) { - $scope.$apply(function () { - const value = pip.getAttribute('data-value'); - $scope.consentTooltip.show = true; - $scope.consentTooltip.event = e; - $scope.consentTooltip.description = $sce.trustAsHtml($scope.installer.current.model.consentLevels[value - 1].description); - }); - }); - - pip.addEventListener('mouseleave', function () { - $scope.$apply(function () { - $scope.consentTooltip.show = false; - $scope.consentTooltip.event = null; - $scope.consentTooltip.description = ''; - }); - }); pip.addEventListener('click', function () { const value = pip.getAttribute('data-value'); @@ -99,8 +79,16 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", f }; function onChangeConsent(values) { - const result = Number(values[0]); - $scope.installer.current.model.telemetryLevel = $scope.installer.current.model.consentLevels[result - 1].level; + const result = Number(values[0]) - 1; + $scope.$apply(() => { + setTelemetryLevelAndDescription(result); + }); }; + function setTelemetryLevelAndDescription(idx) { + $scope.telemetryDescription = $sce.trustAsHtml($scope.installer.current.model.consentLevels[idx].description); + $scope.installer.current.model.telemetryIndex = idx; + $scope.installer.current.model.telemetryLevel = $scope.installer.current.model.consentLevels[idx].level; + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index f0cb8ec1db..225d1feda5 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -1,102 +1,168 @@

Install Umbraco

-

Enter your name, email and password to install Umbraco with its default settings, alternatively you can customize - your installation

+

+ Enter credentials for the default administrator user and choose the level of + consent for telemetry data of your Umbraco installation. +

-
- -
-
-
-
- -
- -
+ +
+
+
+ +
+
+
-
- -
- - Your email will be used as your login -
+
+ +
+ + Your email will be used as your login
+
-
- -
- - - At least {{installer.current.model.minCharLength}} characters long +
+ +
+ + + At least {{installer.current.model.minCharLength}} characters + long - - At least {{installer.current.model.minNonAlphaNumericLength}} - symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} - -
+ + At least {{installer.current.model.minNonAlphaNumericLength}} + symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' + : ''}} +
+
+
+
+ +
+
+ +
- -
-
-
- - -
-
+ +
+
+ Provider: + {{installer.current.model.quickInstallSettings.displayName}} +
Name: + {{installer.current.model.quickInstallSettings.defaultDatabaseName}}
+
+
-
-
- +
+
+ +
+
+
- -
-
- -
-
- Provider: {{installer.current.model.quickInstallSettings.displayName}} -
Name: {{installer.current.model.quickInstallSettings.defaultDatabaseName}}
-
-
-
- -
-
-
- - -
- - -
-
- +

+ + In order to improve Umbraco and add new functionality based on as + relevant information as possible, we would like to collect system- + and usage information from your installation. + +

+

- +
+
+ + + +
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 61895ff793..29ea8f4276 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -29,27 +29,25 @@ display: none !important; } - -html { +body { background: url('../img/installer.svg') no-repeat center center fixed; background-size: cover; -} - -body { margin: 0; padding: 0; - height: 100%; + height: 100vh; width: 100%; font-family: @baseFontFamily; font-size: @baseFontSize; line-height: @baseLineHeight; color: @textColor; - vertical-align: middle; text-align: center; // better font rendering -webkit-font-smoothing: antialiased; font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + // center items + display: grid; + place-items: center center; } #logo { @@ -61,16 +59,14 @@ body { } #installer { - margin: auto; background: @white; - width: 80%; - max-width: 750px; - height: 640px; + width: min-content; + min-width: 500px; text-align: left; - padding: 30px; + padding: 3rem; z-index: 667; border-radius: 6px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.16); + box-shadow: 0 0 2px 4px rgba(0, 0, 0, 0.16); } #overlay { @@ -144,6 +140,14 @@ legend { font-weight: bold } +#installer input { + width: 100%; + + &[type=checkbox] { + width: auto; + } +} + input.ng-dirty.ng-invalid { border-color: @pink; color: @pink; @@ -153,6 +157,21 @@ input.ng-dirty.ng-invalid { opacity: 0.6; } +.installer-cols { + display: grid; + grid-template-columns: repeat(2, minmax(300px, 1fr)); + gap: 120px; +} + +.user-col { + display: flex; + flex-direction: column; +} + +.telemetry-col { + min-height: 500px +} + #installer label.control-label, #installer .constrol-label { @@ -166,6 +185,19 @@ input.ng-dirty.ng-invalid { display: block; color: @gray-3; } + + &.-with-border { + border: 1px solid @inputBorder; + } +} + +.control-actions { + margin-top: 2rem; +} + +.controls-space-between { + display: flex; + justify-content: space-between;; } #installer .input-readonly-text { @@ -176,15 +208,6 @@ input.ng-dirty.ng-invalid { clear: both; } -.absolute-center { - margin: auto; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; -} - .fade-hide, .fade-show { transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; @@ -282,10 +305,12 @@ select { } #consentSliderWrapper { - margin-bottom: 40px; + margin-bottom: 60px; } #consentSlider { + width: 300px; + .noUi-target { background: linear-gradient(to bottom, @grayLighter 0%, @grayLighter 100%); box-shadow: none; diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js index 094941b63a..19fcfc3c2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js @@ -24,6 +24,7 @@ "min": 1, "max": 3 }, + behaviour: 'smooth-steps-tap', pips: { mode: 'values', density: 50, diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html index 10438a4565..d7fed23d8c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html @@ -2,7 +2,7 @@

- Consent for analytics + Consent for telemetry data

@@ -23,29 +23,24 @@
- {{vm.sliderVal}} -
We'll only send an anonymous site ID to let us know that the site exists.
- {{vm.sliderVal}} -
We'll send site ID, umbraco version and packages installed
- {{vm.sliderVal}} -
- We will send: -
- Anonymized site ID, umbraco version, and packages installed. -
- Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use. -
- System information: Webserver, server OS, server framework, server OS language, and database provider. -
- Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode. -
-
We might change what we send on the Detailed level in the future. If so, it will be listed above. + We will send: +
    +
  • Anonymized site ID, umbraco version, and packages installed.
  • +
  • Number of: Root nodes, Content nodes, Macros, Media, Document Types, Templates, Languages, Domains, User Group, Users, Members, and Property Editors in use.
  • +
  • System information: Webserver, server OS, server framework, server OS language, and database provider.
  • +
  • Configuration settings: Modelsbuilder mode, if custom Umbraco path exists, ASP environment, and if you are in debug mode.
  • +
+ We might change what we send on the Detailed level in the future. If so, it will be listed above.
By choosing "Detailed" you agree to current and future anonymized information being collected.
From 636ee831a409294c7868c511fe0da8273bf19ffc Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 2 Aug 2022 11:00:58 +0200 Subject: [PATCH 098/101] Force the allowed avatar image types. We do not want to use the Umbraco:Cms:Content:Imaging:ImageFileTypes as this could very well be different for content. (cherry picked from commit 6ce9ea4950b8a053e49bd3adec7bedb8d6a2d4a5) --- src/Umbraco.Web.BackOffice/Controllers/UsersController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index 24e5a77a23..f734d8626b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -179,8 +179,9 @@ public class UsersController : BackOfficeNotificationsController var fileName = file.FileName.Trim(new[] { '\"' }).TrimEnd(); var safeFileName = fileName.ToSafeFileName(shortStringHelper); var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + const string allowedAvatarFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp"; - if (contentSettings.DisallowedUploadFiles.Contains(ext) == false) + if (allowedAvatarFileTypes.Contains(ext) == true && contentSettings.DisallowedUploadFiles.Contains(ext) == false) { //generate a path of known data, we don't want this path to be guessable user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash() + "." + ext; From 6ce91eff4e449f8e9388e717c9384fcb6bd0998d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 2 Aug 2022 13:10:20 +0200 Subject: [PATCH 099/101] Unbreak breaking changes --- .../Persistence/Repositories/IRedirectUrlRepository.cs | 4 ++-- src/Umbraco.Core/Services/IRedirectUrlService.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs index 643c248d64..b7f29edc92 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs @@ -46,7 +46,7 @@ public interface IRedirectUrlRepository : IReadWriteQueryRepository /// The Umbraco redirect URL route. /// The most recent redirect URL corresponding to the route. - Task GetMostRecentUrlAsync(string url); + Task GetMostRecentUrlAsync(string url) => Task.FromResult(GetMostRecentUrl(url)); /// /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route. @@ -62,7 +62,7 @@ public interface IRedirectUrlRepository : IReadWriteQueryRepositoryThe Umbraco redirect URL route. /// The culture the domain is associated with /// The most recent redirect URL corresponding to the route. - Task GetMostRecentUrlAsync(string url, string culture); + Task GetMostRecentUrlAsync(string url, string culture) => Task.FromResult(GetMostRecentUrl(url, culture)); /// /// Gets all redirect URLs for a content item. diff --git a/src/Umbraco.Core/Services/IRedirectUrlService.cs b/src/Umbraco.Core/Services/IRedirectUrlService.cs index f40d5fbf75..f1897053f0 100644 --- a/src/Umbraco.Core/Services/IRedirectUrlService.cs +++ b/src/Umbraco.Core/Services/IRedirectUrlService.cs @@ -60,7 +60,7 @@ public interface IRedirectUrlService : IService /// The Umbraco redirect URL route. /// The culture of the request. /// The most recent redirect URLs corresponding to the route. - Task GetMostRecentRedirectUrlAsync(string url, string? culture); + Task GetMostRecentRedirectUrlAsync(string url, string? culture) => Task.FromResult(GetMostRecentRedirectUrl(url, culture)); /// /// Gets all redirect URLs for a content item. From 47c1f7b22b902a6826979d09894821c08cd9161d Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 2 Aug 2022 13:39:21 +0200 Subject: [PATCH 100/101] Added CompatibilitySuppressions.xml to Examine.Lucene project --- .../CompatibilitySuppressions.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml diff --git a/src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml b/src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml new file mode 100644 index 0000000000..681c7cdab5 --- /dev/null +++ b/src/Umbraco.Examine.Lucene/CompatibilitySuppressions.xml @@ -0,0 +1,10 @@ + + + + CP0008 + T:Umbraco.Cms.Infrastructure.Examine.UmbracoContentIndex + lib/net6.0/Umbraco.Examine.Lucene.dll + lib/net6.0/Umbraco.Examine.Lucene.dll + true + + \ No newline at end of file From c5654f031195544d6bbb9d8b27c21458ec692890 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 15 Jul 2022 11:56:25 +0200 Subject: [PATCH 101/101] Keep CopyRazorGenerateFilesToPublishDirectory and update comments --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 ++++++---- src/Umbraco.Web.UI/appsettings.template.json | 3 +-- .../UmbracoProject/UmbracoProject.csproj | 24 +++++++++---------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index cf86519c8e..2ec54bc2d8 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -16,17 +16,21 @@ - + - + - + + true + + + + false false - true diff --git a/src/Umbraco.Web.UI/appsettings.template.json b/src/Umbraco.Web.UI/appsettings.template.json index ed03407c0b..1ac02f8904 100644 --- a/src/Umbraco.Web.UI/appsettings.template.json +++ b/src/Umbraco.Web.UI/appsettings.template.json @@ -68,8 +68,7 @@ "EnableTours": true }, "ModelsBuilder": { - "ModelsMode": "InMemoryAuto", - "Enable": true + "ModelsMode": "InMemoryAuto" } } } diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index 574ff04452..2880b754e6 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -10,25 +10,25 @@ - + + + + true + + + + + false + false + + - - - - - - - - false - false - true -