diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 3ff37ac79c..ff195a37ee 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -103,7 +103,7 @@ Great question! The short version goes like this:
1. **Switch to the correct branch**
- Switch to the `v11/contrib` branch
+ Switch to the `contrib` branch
1. **Build**
@@ -111,7 +111,7 @@ Great question! The short version goes like this:
1. **Branch**
- 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 `v11/contrib`, create a new branch first.
+ 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 `contrib`, create a new branch first.
1. **Change**
@@ -121,7 +121,7 @@ Great question! The short version goes like this:
Done? Yay! 🎉
- Remember to commit to your new `temp` branch, and don't commit to `v11/contrib`. Then you can push the changes up to your fork on GitHub.
+ Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
#### Keeping your Umbraco fork in sync with the main repository
[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
@@ -138,10 +138,10 @@ Then when you want to get the changes from the main repository:
```
git fetch upstream
-git rebase upstream/v11/contrib
+git rebase upstream/contrib
```
-In this command we're syncing with the `v11/contrib` branch, but you can of course choose another one if needed.
+In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
@@ -169,7 +169,7 @@ We recommend you to [sync with our repository][sync fork] before you submit your
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.

-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 `v11/contrib`. If you are working on v9, this is the branch you should be targeting.
+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 `contrib`. 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.
diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
index 4876349217..38c3bedc03 100644
--- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
+++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj
@@ -12,10 +12,10 @@
-
+
-
+
diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
index 64ed9e3566..a1b9d2d8f0 100644
--- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
+++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerSyntaxProvider.cs
@@ -272,6 +272,13 @@ where tbl.[name]=@0 and col.[name]=@1;",
return !constraintName.IsNullOrWhiteSpace();
}
+ public override bool DoesPrimaryKeyExist(IDatabase db, string tableName, string primaryKeyName)
+ {
+ IEnumerable? keys = db.Fetch($"select * from sysobjects where xtype='pk' and parent_obj in (select id from sysobjects where name='{tableName}')")
+ .Where(x => x.Name == primaryKeyName);
+ return keys.FirstOrDefault() is not null;
+ }
+
public override bool DoesTableExist(IDatabase db, string tableName)
{
var result =
@@ -453,4 +460,9 @@ _sqlInspector ??= new SqlInspectionUtilities();
}
#endregion
+
+ private class SqlPrimaryKey
+ {
+ public string Name { get; set; } = null!;
+ }
}
diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs
index e00af97909..7359b122e1 100644
--- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs
+++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs
@@ -176,6 +176,14 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase
return false;
}
+ public override bool DoesPrimaryKeyExist(IDatabase db, string tableName, string primaryKeyName)
+ {
+ IEnumerable items = db.Fetch($"select sql from sqlite_master where type = 'table' and name = '{tableName}'")
+ .Where(x => x.Contains($"CONSTRAINT {primaryKeyName} PRIMARY KEY"));
+
+ return items.Any();
+ }
+
public override string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null)
{
var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo;
diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj
index 8fb7385195..d9f9ac5123 100644
--- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj
+++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj
index 922be06acc..f627057d24 100644
--- a/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj
+++ b/src/Umbraco.Cms.Targets/Umbraco.Cms.Targets.csproj
@@ -24,7 +24,7 @@
NU5100;NU5128
-
+
diff --git a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs
index bec6d77bfb..a59b9bf861 100644
--- a/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/UmbracoPluginSettings.cs
@@ -19,7 +19,7 @@ public class UmbracoPluginSettings
".css", // styles
".js", // scripts
".jpg", ".jpeg", ".gif", ".png", ".svg", // images
- ".eot", ".ttf", ".woff", // fonts
+ ".eot", ".ttf", ".woff", ".woff2", // fonts
".xml", ".json", ".config", // configurations
".lic", // license
".map", // js map files
diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs
index 1ad94cbdc3..03ed07f2fe 100644
--- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs
+++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs
@@ -232,9 +232,7 @@ public static class UdiGetterExtensions
throw new ArgumentNullException("entity");
}
- return new StringUdi(
- Constants.UdiEntityType.Stylesheet,
- entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed();
+ return GetUdiFromPath(Constants.UdiEntityType.Stylesheet, entity.Path);
}
///
@@ -249,8 +247,15 @@ public static class UdiGetterExtensions
throw new ArgumentNullException("entity");
}
- return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash))
- .EnsureClosed();
+ return GetUdiFromPath(Constants.UdiEntityType.Script, entity.Path);
+ }
+
+ private static StringUdi GetUdiFromPath(string entityType, string path)
+ {
+ var id = path
+ .TrimStart(Constants.CharArrays.ForwardSlash)
+ .Replace("\\", "/");
+ return new StringUdi(entityType, id).EnsureClosed();
}
///
@@ -300,7 +305,7 @@ public static class UdiGetterExtensions
? Constants.UdiEntityType.PartialViewMacro
: Constants.UdiEntityType.PartialView;
- return new StringUdi(entityType, entity.Path.TrimStart(Constants.CharArrays.ForwardSlash)).EnsureClosed();
+ return GetUdiFromPath(entityType, entity.Path);
}
///
diff --git a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs
index 30cf2e6016..2e6f29ff37 100644
--- a/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs
+++ b/src/Umbraco.Core/Media/EmbedProviders/Youtube.cs
@@ -14,7 +14,7 @@ public class YouTube : OEmbedProviderBase
public override string ApiEndpoint => "https://www.youtube.com/oembed";
- public override string[] UrlSchemeRegex => new[] { @"youtu.be/.*", @"youtube.com/watch.*" };
+ public override string[] UrlSchemeRegex => new[] { @"youtu.be/.*", @"youtube.com/watch.*", @"youtube.com/shorts/.*" };
public override Dictionary RequestParams => new()
{
diff --git a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs
index f06c053d14..0c46ce02ef 100644
--- a/src/Umbraco.Core/Models/DictionaryItemExtensions.cs
+++ b/src/Umbraco.Core/Models/DictionaryItemExtensions.cs
@@ -16,15 +16,6 @@ public static class DictionaryItemExtensions
return trans == null ? string.Empty : trans.Value;
}
- ///
- /// Returns the translation value for the language, if no translation is found it returns an empty string
- ///
- ///
- ///
- ///
- public static string? GetTranslatedValue(this IDictionaryItem d, ILanguage language)
- => d.GetTranslatedValue(language.IsoCode);
-
///
/// Adds or updates a translation for a dictionary item and language
///
diff --git a/src/Umbraco.Core/Models/ILanguage.cs b/src/Umbraco.Core/Models/ILanguage.cs
index 5af66089ca..eeacc86ef7 100644
--- a/src/Umbraco.Core/Models/ILanguage.cs
+++ b/src/Umbraco.Core/Models/ILanguage.cs
@@ -55,6 +55,7 @@ public interface ILanguage : IEntity, IRememberBeingDirty
/// define fallback strategies when a value does not exist for a requested language.
///
///
+ [Obsolete("This will be replaced by fallback language ISO code in V13.")]
[DataMember]
public string? FallbackIsoCode { get; set; }
}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 329313fe85..2222b50b52 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -8,10 +8,10 @@
-
+
-
+
@@ -24,7 +24,7 @@
-
+
diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj
index 61b7446724..5130e9cc01 100644
--- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj
+++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj
@@ -7,7 +7,7 @@
-
+
diff --git a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs
index a8444f4276..5e2130b5d3 100644
--- a/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs
+++ b/src/Umbraco.Infrastructure/Logging/Viewer/ExpressionFilter.cs
@@ -29,7 +29,7 @@ internal class ExpressionFilter : ILogFilter
// If the expression is one word and doesn't contain a serilog operator then we can perform a like search
if (!filterExpression.Contains(" ") && !filterExpression.ContainsAny(ExpressionOperators.Select(c => c)))
{
- filter = PerformMessageLikeFilter(filterExpression);
+ filter = PerformMessageLikeFilter(filterExpression, customSerilogFunctions);
}
// check if it's a valid expression
@@ -48,7 +48,7 @@ internal class ExpressionFilter : ILogFilter
{
// 'error' describes a syntax error, where it was unable to compile an expression
// Assume the expression was a search string and make a Like filter from that
- filter = PerformMessageLikeFilter(filterExpression);
+ filter = PerformMessageLikeFilter(filterExpression, customSerilogFunctions);
}
}
@@ -57,10 +57,10 @@ internal class ExpressionFilter : ILogFilter
public bool TakeLogEvent(LogEvent e) => _filter == null || _filter(e);
- private Func? PerformMessageLikeFilter(string filterExpression)
+ private Func? PerformMessageLikeFilter(string filterExpression, SerilogLegacyNameResolver serilogLegacyNameResolver)
{
var filterSearch = $"@Message like '%{SerilogExpression.EscapeLikeExpressionContent(filterExpression)}%'";
- if (SerilogExpression.TryCompile(filterSearch, out CompiledExpression? compiled, out var error))
+ if (SerilogExpression.TryCompile(filterSearch, null, serilogLegacyNameResolver, out CompiledExpression? compiled, out var error))
{
// `compiled` is a function that can be executed against `LogEvent`s:
return evt =>
diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs
index 49775bcd0a..b548112d8f 100644
--- a/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs
+++ b/src/Umbraco.Infrastructure/Migrations/MigrationBase_Extra.cs
@@ -110,6 +110,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations
return indexes.Any(x => x.Item2.InvariantEquals(indexName));
}
+ protected bool PrimaryKeyExists(string tableName, string primaryKeyName)
+ {
+ return SqlSyntax.DoesPrimaryKeyExist(Context.Database, tableName, primaryKeyName);
+ }
+
protected bool ColumnExists(string tableName, string columnName)
{
ColumnInfo[]? columns = SqlSyntax.GetColumnsInSchema(Context.Database).Distinct().ToArray();
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs
index 331a6fced6..d1185bec46 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs
@@ -10,18 +10,28 @@ public class AddPrimaryKeyConstrainToContentVersionCleanupDtos : MigrationBase
protected override void Migrate()
{
- IEnumerable contentVersionCleanupPolicyDtos =
- Database
- .Fetch()
- .OrderByDescending(x => x.Updated)
- .DistinctBy(x => x.ContentTypeId);
-
+ IEnumerable? contentVersionCleanupPolicyDtos = null;
if (TableExists(ContentVersionCleanupPolicyDto.TableName))
{
+ if (PrimaryKeyExists(ContentVersionCleanupPolicyDto.TableName, "PK_umbracoContentVersionCleanupPolicy"))
+ {
+ return;
+ }
+
+ contentVersionCleanupPolicyDtos =
+ Database
+ .Fetch()
+ .OrderByDescending(x => x.Updated)
+ .DistinctBy(x => x.ContentTypeId);
+
Delete.Table(ContentVersionCleanupPolicyDto.TableName).Do();
}
Create.Table().Do();
- Database.InsertBatch(contentVersionCleanupPolicyDtos);
+
+ if (contentVersionCleanupPolicyDtos is not null)
+ {
+ Database.InsertBatch(contentVersionCleanupPolicyDtos);
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs
index 128c59594b..4dd1a14fb5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs
@@ -1,4 +1,3 @@
-using System.Data;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
@@ -13,7 +12,7 @@ internal class ContentVersionCleanupPolicyDto
public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCleanupPolicy;
[Column("contentTypeId")]
- [PrimaryKeyColumn(AutoIncrement = false)]
+ [PrimaryKeyColumn(AutoIncrement = false, Name = "PK_umbracoContentVersionCleanupPolicy")]
[ForeignKey(typeof(ContentTypeDto), Column = "nodeId")]
public int ContentTypeId { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs
index c5829873fe..5576b5ca43 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs
@@ -5,6 +5,7 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos;
+
[TableName(Constants.DatabaseSchema.Tables.KeyValue)]
[PrimaryKey("key", AutoIncrement = false)]
[ExplicitColumns]
@@ -22,4 +23,6 @@ internal class KeyValueDto
[Column("updated")]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime UpdateDate { get; set; }
+
+ //NOTE that changes to this file needs to be backward compatible. Otherwise our upgrader cannot work, as it uses this to read from the db
}
diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs
index d9b76a4942..a71ccf5bed 100644
--- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs
+++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs
@@ -188,6 +188,8 @@ public interface ISqlSyntaxProvider
///
bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName);
+ bool DoesPrimaryKeyExist(IDatabase db, string tableName, string primaryKeyName) => throw new NotImplementedException();
+
string GetFieldNameForUpdate(Expression> fieldSelector, string? tableAlias = null);
///
diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs
index 48b882d604..15cb68bfc5 100644
--- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs
@@ -209,6 +209,8 @@ public abstract class SqlSyntaxProviderBase : ISqlSyntaxProvider
public abstract bool TryGetDefaultConstraint(IDatabase db, string? tableName, string columnName, [MaybeNullWhen(false)] out string constraintName);
+ public virtual bool DoesPrimaryKeyExist(IDatabase db, string tableName, string primaryKeyName) => throw new NotImplementedException();
+
public virtual string GetFieldNameForUpdate(
Expression> fieldSelector,
string? tableAlias = null) => this.GetFieldName(fieldSelector, tableAlias);
diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj
index 929627fc86..d11d61c5a8 100644
--- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj
+++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj
@@ -11,17 +11,17 @@
-
+
-
-
+
+
-
+
diff --git a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj
index 2a68119f16..4bbdcc097a 100644
--- a/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj
+++ b/src/Umbraco.PublishedCache.NuCache/Umbraco.PublishedCache.NuCache.csproj
@@ -10,6 +10,8 @@
+
+
diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
index 7704344d4e..97aa5bd118 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
@@ -423,7 +423,7 @@ public class AuthenticationController : UmbracoApiControllerBase
var mailMessage = new EmailMessage(from, user.Email, subject, message, true);
- await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset);
+ await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.PasswordReset, true);
_userManager.NotifyForgotPasswordRequested(User, user.Id.ToString());
}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
index 42019f7c99..fe88701266 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/EntityController.cs
@@ -1,8 +1,11 @@
using System.Collections.Concurrent;
using System.Dynamic;
using System.Globalization;
+using System.IO;
using System.Linq.Expressions;
using System.Reflection;
+using System.Security.Cryptography;
+using Examine.Search;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
@@ -15,6 +18,7 @@ using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Models.TemplateQuery;
+using Umbraco.Cms.Core.Persistence;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -745,7 +749,11 @@ public class EntityController : UmbracoAuthorizedJsonController
{
return new PagedResult(0, 0, 0);
}
-
+ //adding multiple conditions ,considering id,key & name as filter param
+ //for id as int
+ int.TryParse(filter, out int filterAsIntId);
+ //for key as Guid
+ Guid.TryParse(filter, out Guid filterAsGuid);
// else proceed as usual
entities = _entityService.GetPagedChildren(
id,
@@ -755,7 +763,9 @@ public class EntityController : UmbracoAuthorizedJsonController
out long totalRecords,
filter.IsNullOrWhiteSpace()
? null
- : _sqlContext.Query().Where(x => x.Name!.Contains(filter)),
+ : _sqlContext.Query().Where(x => x.Name!.Contains(filter)
+ || x.Id == filterAsIntId
+ || x.Key == filterAsGuid),
Ordering.By(orderBy, orderDirection));
diff --git a/src/Umbraco.Web.Common/Controllers/MaintenanceModeActionFilterAttribute.cs b/src/Umbraco.Web.Common/Controllers/MaintenanceModeActionFilterAttribute.cs
index 3c4f82f25d..4f5877a765 100644
--- a/src/Umbraco.Web.Common/Controllers/MaintenanceModeActionFilterAttribute.cs
+++ b/src/Umbraco.Web.Common/Controllers/MaintenanceModeActionFilterAttribute.cs
@@ -10,9 +10,8 @@ namespace Umbraco.Cms.Web.Common.Controllers;
internal sealed class MaintenanceModeActionFilterAttribute : TypeFilterAttribute
{
- public MaintenanceModeActionFilterAttribute() : base(typeof(MaintenanceModeActionFilter))
- {
- }
+
+ public MaintenanceModeActionFilterAttribute() : base(typeof(MaintenanceModeActionFilter)) => Order = int.MinValue; // Ensures this run as the first filter.
private sealed class MaintenanceModeActionFilter : IActionFilter
{
diff --git a/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs b/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs
index 95b8bc7320..10986d3882 100644
--- a/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs
+++ b/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs
@@ -15,6 +15,12 @@ internal class PublishedRequestFilterAttribute : ResultFilterAttribute
///
public override void OnResultExecuting(ResultExecutingContext context)
{
+ if (context.Result is not null)
+ {
+ // If the result is already set, we just skip the execution
+ return;
+ }
+
UmbracoRouteValues routeVals = GetUmbracoRouteValues(context);
IPublishedRequest pcr = routeVals.PublishedRequest;
diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs
index c59f0bd86e..46d07deb88 100644
--- a/src/Umbraco.Web.Common/Security/MemberManager.cs
+++ b/src/Umbraco.Web.Common/Security/MemberManager.cs
@@ -50,7 +50,7 @@ public class MemberManager : UmbracoUserManager
- public async Task IsMemberAuthorizedAsync(
+ public virtual async Task IsMemberAuthorizedAsync(
IEnumerable? allowTypes = null,
IEnumerable? allowGroups = null,
IEnumerable? allowMembers = null)
@@ -122,14 +122,14 @@ public class MemberManager : UmbracoUserManager
- public bool IsLoggedIn()
+ public virtual bool IsLoggedIn()
{
HttpContext? httpContext = _httpContextAccessor.HttpContext;
return httpContext?.User.Identity?.IsAuthenticated ?? false;
}
///
- public async Task MemberHasAccessAsync(string path)
+ public virtual async Task MemberHasAccessAsync(string path)
{
if (await IsProtectedAsync(path))
{
@@ -140,7 +140,7 @@ public class MemberManager : UmbracoUserManager
- public async Task> MemberHasAccessAsync(IEnumerable paths)
+ public virtual async Task> MemberHasAccessAsync(IEnumerable paths)
{
IReadOnlyDictionary protectedPaths = await IsProtectedAsync(paths);
@@ -163,10 +163,10 @@ public class MemberManager : UmbracoUserManager
/// this is a cached call
///
- public Task IsProtectedAsync(string path) => Task.FromResult(_publicAccessService.IsProtected(path).Success);
+ public virtual Task IsProtectedAsync(string path) => Task.FromResult(_publicAccessService.IsProtected(path).Success);
///
- public Task> IsProtectedAsync(IEnumerable paths)
+ public virtual Task> IsProtectedAsync(IEnumerable paths)
{
var result = new Dictionary();
foreach (var path in paths)
@@ -179,7 +179,7 @@ public class MemberManager : UmbracoUserManager
- public async Task GetCurrentMemberAsync()
+ public virtual async Task GetCurrentMemberAsync()
{
if (_currentMember == null)
{
@@ -194,7 +194,7 @@ public class MemberManager : UmbracoUserManager _store.GetPublishedMember(user);
+ public virtual IPublishedContent? AsPublishedMember(MemberIdentityUser user) => _store.GetPublishedMember(user);
///
/// This will check if the member has access to this path
diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
index 7a54911735..6b392c42fd 100644
--- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
+++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
@@ -126,7 +126,7 @@ public class MemberSignInManager : UmbracoSignInManager, IMe
///
/// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking
///
- public async Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false)
+ public virtual async Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// to be able to deal with auto-linking and reduce duplicate lookups
diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
index 7fbb413115..8feb0ced58 100644
--- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
+++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj
@@ -12,8 +12,8 @@
-
-
+
+
diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json
index 21780e8bb2..9d8a759ec5 100644
--- a/src/Umbraco.Web.UI.Client/package-lock.json
+++ b/src/Umbraco.Web.UI.Client/package-lock.json
@@ -9,7 +9,7 @@
"@microsoft/signalr": "7.0.2",
"@umbraco-ui/uui": "1.1.0",
"@umbraco-ui/uui-css": "1.1.0",
- "ace-builds": "1.14.0",
+ "ace-builds": "1.15.0",
"angular": "1.8.3",
"angular-animate": "1.8.3",
"angular-aria": "1.8.3",
@@ -37,7 +37,7 @@
"lazyload-js": "1.0.0",
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
- "nouislider": "15.6.1",
+ "nouislider": "15.7.0",
"spectrum-colorpicker2": "2.0.9",
"tinymce": "6.3.1",
"typeahead.js": "0.11.1",
@@ -49,7 +49,7 @@
"@babel/preset-env": "7.20.2",
"autoprefixer": "10.4.13",
"cssnano": "5.1.14",
- "eslint": "8.32.0",
+ "eslint": "8.33.0",
"gulp": "4.0.2",
"gulp-angular-embed-templates": "2.3.0",
"gulp-babel": "8.0.0",
@@ -69,7 +69,7 @@
"gulp-wrap": "0.15.0",
"gulp-wrap-js": "0.4.1",
"jasmine-core": "4.5.0",
- "jsdom": "21.0.0",
+ "jsdom": "21.1.0",
"karma": "6.4.1",
"karma-jasmine": "5.1.0",
"karma-jsdom-launcher": "14.0.0",
@@ -2877,9 +2877,9 @@
}
},
"node_modules/ace-builds": {
- "version": "1.14.0",
- "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.14.0.tgz",
- "integrity": "sha512-3q8LvawomApRCt4cC0OzxVjDsZ609lDbm8l0Xl9uqG06dKEq4RT0YXLUyk7J2SxmqIp5YXzZNw767Dr8GKUruw=="
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.15.0.tgz",
+ "integrity": "sha512-L1RXgqxDvzbJ7H8Y2v9lb4kHaZRn5JNTECG+oZTH2EDewMmpQMLDC4GnFKIh3+xb/gk2nVPO7gGwpTYPw91QzA=="
},
"node_modules/acorn": {
"version": "8.8.1",
@@ -6434,9 +6434,9 @@
}
},
"node_modules/eslint": {
- "version": "8.32.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.32.0.tgz",
- "integrity": "sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==",
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz",
+ "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.4.1",
@@ -10638,9 +10638,9 @@
}
},
"node_modules/jsdom": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.0.0.tgz",
- "integrity": "sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA==",
+ "version": "21.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.0.tgz",
+ "integrity": "sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg==",
"dev": true,
"dependencies": {
"abab": "^2.0.6",
@@ -12543,9 +12543,9 @@
}
},
"node_modules/nouislider": {
- "version": "15.6.1",
- "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.6.1.tgz",
- "integrity": "sha512-1T5AfeEMGrGM87UJ+qAHvauPfCe/woOjYV/o29fp21+XgGuGpkM1Udo7mPHnidu4+cxlj35rDBWKiA6Mefemrg=="
+ "version": "15.7.0",
+ "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.7.0.tgz",
+ "integrity": "sha512-aJVEULBPOUwq32/s7xnLNyLvo4kuzYJJsNp2PNGW932AQ0uuDAbLShAqswtxRzJc5n/dLJXNlYSLOZ57bcUg1w=="
},
"node_modules/now-and-later": {
"version": "2.0.1",
@@ -16459,9 +16459,9 @@
"dev": true
},
"node_modules/ua-parser-js": {
- "version": "0.7.31",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
- "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
+ "version": "0.7.33",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
+ "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==",
"dev": true,
"funding": [
{
@@ -19612,9 +19612,9 @@
}
},
"ace-builds": {
- "version": "1.14.0",
- "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.14.0.tgz",
- "integrity": "sha512-3q8LvawomApRCt4cC0OzxVjDsZ609lDbm8l0Xl9uqG06dKEq4RT0YXLUyk7J2SxmqIp5YXzZNw767Dr8GKUruw=="
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.15.0.tgz",
+ "integrity": "sha512-L1RXgqxDvzbJ7H8Y2v9lb4kHaZRn5JNTECG+oZTH2EDewMmpQMLDC4GnFKIh3+xb/gk2nVPO7gGwpTYPw91QzA=="
},
"acorn": {
"version": "8.8.1",
@@ -22503,9 +22503,9 @@
}
},
"eslint": {
- "version": "8.32.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.32.0.tgz",
- "integrity": "sha512-nETVXpnthqKPFyuY2FNjz/bEd6nbosRgKbkgS/y1C7LJop96gYHWpiguLecMHQ2XCPxn77DS0P+68WzG6vkZSQ==",
+ "version": "8.33.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.33.0.tgz",
+ "integrity": "sha512-WjOpFQgKK8VrCnAtl8We0SUOy/oVZ5NHykyMiagV1M9r8IFpIJX7DduK6n1mpfhlG7T1NLWm2SuD8QB7KFySaA==",
"dev": true,
"requires": {
"@eslint/eslintrc": "^1.4.1",
@@ -25756,9 +25756,9 @@
}
},
"jsdom": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.0.0.tgz",
- "integrity": "sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA==",
+ "version": "21.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-21.1.0.tgz",
+ "integrity": "sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg==",
"dev": true,
"requires": {
"abab": "^2.0.6",
@@ -27241,9 +27241,9 @@
"dev": true
},
"nouislider": {
- "version": "15.6.1",
- "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.6.1.tgz",
- "integrity": "sha512-1T5AfeEMGrGM87UJ+qAHvauPfCe/woOjYV/o29fp21+XgGuGpkM1Udo7mPHnidu4+cxlj35rDBWKiA6Mefemrg=="
+ "version": "15.7.0",
+ "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.7.0.tgz",
+ "integrity": "sha512-aJVEULBPOUwq32/s7xnLNyLvo4kuzYJJsNp2PNGW932AQ0uuDAbLShAqswtxRzJc5n/dLJXNlYSLOZ57bcUg1w=="
},
"now-and-later": {
"version": "2.0.1",
@@ -30236,9 +30236,9 @@
"dev": true
},
"ua-parser-js": {
- "version": "0.7.31",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
- "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==",
+ "version": "0.7.33",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
+ "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==",
"dev": true
},
"unbox-primitive": {
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index c7f022d53b..b57ca68ba1 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -21,7 +21,7 @@
"@microsoft/signalr": "7.0.2",
"@umbraco-ui/uui": "1.1.0",
"@umbraco-ui/uui-css": "1.1.0",
- "ace-builds": "1.14.0",
+ "ace-builds": "1.15.0",
"angular": "1.8.3",
"angular-animate": "1.8.3",
"angular-aria": "1.8.3",
@@ -49,7 +49,7 @@
"lazyload-js": "1.0.0",
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
- "nouislider": "15.6.1",
+ "nouislider": "15.7.0",
"spectrum-colorpicker2": "2.0.9",
"tinymce": "6.3.1",
"typeahead.js": "0.11.1",
@@ -61,7 +61,7 @@
"@babel/preset-env": "7.20.2",
"autoprefixer": "10.4.13",
"cssnano": "5.1.14",
- "eslint": "8.32.0",
+ "eslint": "8.33.0",
"gulp": "4.0.2",
"gulp-angular-embed-templates": "2.3.0",
"gulp-babel": "8.0.0",
@@ -81,7 +81,7 @@
"gulp-wrap": "0.15.0",
"gulp-wrap-js": "0.4.1",
"jasmine-core": "4.5.0",
- "jsdom": "21.0.0",
+ "jsdom": "21.1.0",
"karma": "6.4.1",
"karma-jasmine": "5.1.0",
"karma-jsdom-launcher": "14.0.0",
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js
index 5d90958dbd..2c561d3505 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js
@@ -69,8 +69,9 @@ Use this directive to render an avatar.
}
function getNameInitials(name) {
- if(name) {
- var names = name.split(' '),
+ if (name) {
+ const notAllowed = /[\[\]\{\}\*\?\&\$\@\!\(\)\%\#]+/g;
+ var names = name.replace(notAllowed,'').trim().split(' '),
initials = names[0].substring(0, 1);
if (names.length > 1) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
index 1841548426..fc84e53979 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
@@ -3,21 +3,8 @@
* @name umbraco.directives.directive:umbFileDropzone
* @restrict E
* @function
-* @description
+* @description Show a dropzone that allows the user to drag files and have them be uploaded to the media library
**/
-
-/*
-TODO
-.directive("umbFileDrop", function ($timeout, $upload, localizationService, umbRequestHelper){
- return{
- restrict: "A",
- link: function(scope, element, attrs){
- //load in the options model
- }
- }
-})
-*/
-
angular.module("umbraco.directives")
.directive('umbFileDropzone',
function ($timeout, Upload, localizationService, umbRequestHelper, overlayService, mediaHelper, mediaTypeHelper) {
@@ -34,11 +21,10 @@ angular.module("umbraco.directives")
compact: '@',
hideDropzone: '@',
- acceptedMediatypes: '=',
+ acceptedMediatypes: '<',
- filesQueued: '=',
- handleFile: '=',
- filesUploaded: '='
+ filesQueued: '<',
+ filesUploaded: '<'
},
link: function (scope, element, attrs) {
scope.queue = [];
@@ -66,6 +52,14 @@ angular.module("umbraco.directives")
}
}
+ /**
+ * Initial entrypoint to handle the queued files. It will determine if the files are acceptable
+ * and determine if the user needs to pick a media type
+ * @param files
+ * @param event
+ * @returns void
+ * @private
+ */
function _filesQueued(files, event) {
//Push into the queue
Utilities.forEach(files, file => {
@@ -75,6 +69,10 @@ angular.module("umbraco.directives")
}
});
+ // Add all of the processing and processed files to account for uploading
+ // files in stages (dragging files X at a time into the dropzone).
+ scope.totalQueued = scope.queue.length + scope.processingCount + scope.processed.length;
+
// Upload not allowed
if (!scope.acceptedMediatypes || !scope.acceptedMediatypes.length) {
files.map(file => {
@@ -82,19 +80,27 @@ angular.module("umbraco.directives")
});
}
- // If we have Accepted Media Types, we will ask to choose Media Type, if
- // Choose Media Type returns false, it only had one choice and therefor no reason to
- if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) {
- scope.contentTypeAlias = "umbracoAutoSelect";
+ // If we have Accepted Media Types, we will ask to choose Media Type
+ if (scope.acceptedMediatypes) {
+
+ // If the media type dialog returns a positive answer, it is safe to assume that the
+ // contentTypeAlias has been chosen and we can return early because the dialog will start processing
+ // the queue automatically
+ if (_requestChooseMediaTypeDialog()) {
+ return;
+ }
}
- // Add all of the processing and processed files to account for uploading
- // files in stages (dragging files X at a time into the dropzone).
- scope.totalQueued = scope.queue.length + scope.processingCount + scope.processed.length;
-
+ // Start the processing of the queue here because the media type dialog was not shown and therefore
+ // did not do it earlier
_processQueueItems();
}
+ /**
+ * Run through the queue and start processing files
+ * @returns void
+ * @private
+ */
function _processQueueItems() {
if (scope.processingCount === scope.batchSize) {
@@ -133,10 +139,17 @@ angular.module("umbraco.directives")
}
}
+ /**
+ * Upload a specific file and use the scope.contentTypeAlias for the type or fall back to letting
+ * the backend auto select a type.
+ * @param file
+ * @returns void
+ * @private
+ */
function _upload(file) {
scope.propertyAlias = scope.propertyAlias ? scope.propertyAlias : "umbracoFile";
- scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "Image";
+ scope.contentTypeAlias = scope.contentTypeAlias ? scope.contentTypeAlias : "umbracoAutoSelect";
scope.processingCount++;
@@ -195,6 +208,11 @@ angular.module("umbraco.directives")
});
}
+ /**
+ * Opens the media type dialog and lets the user choose a media type. If the queue is empty it will not show.
+ * @returns {boolean}
+ * @private
+ */
function _requestChooseMediaTypeDialog() {
if (scope.queue.length === 0) {
@@ -218,7 +236,6 @@ angular.module("umbraco.directives")
return false;
}
-
localizationService.localizeMany(["defaultdialogs_selectMediaType", "mediaType_autoPickMediaType"]).then(function (translations) {
filteredMediaTypes.push({
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js
index c69fc60a82..5406927d38 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js
@@ -18,7 +18,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerCo
// NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive.
const defaultColor = "000000";
const defaultLabel = null;
-
+
$scope.newColor = defaultColor;
$scope.newLabel = defaultLabel;
$scope.hasError = false;
@@ -48,20 +48,20 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerCo
}
});
}
-
+
var evts = [];
evts.push(eventsService.on("toggleValue", function (e, args) {
if (args.inputId === "useLabel") {
vm.labelEnabled = args.value;
}
}));
-
+
$scope.$on('$destroy', function () {
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
});
-
+
if (!Utilities.isArray($scope.model.value)) {
//make an array from the dictionary
var items = [];
@@ -127,11 +127,20 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerCo
label: newLabel
});
} else {
+
+ if(vm.editItem.value === vm.editItem.label && vm.editItem.value === newLabel) {
+ vm.editItem.label = $scope.newColor;
+
+ }
+ else {
+ vm.editItem.label = newLabel;
+ }
+
vm.editItem.value = $scope.newColor;
- vm.editItem.label = newLabel;
+
vm.editItem = null;
}
-
+
$scope.newLabel = "";
$scope.hasError = false;
$scope.focusOnNew = true;
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 45614f6d2c..9c36ce7355 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
@@ -80,12 +80,12 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
var idsWithPermissions = null;
$scope.buttonPermissions = {
- canCopy: true,
- canCreate: true,
- canDelete: true,
- canMove: true,
- canPublish: true,
- canUnpublish: true
+ canCopy: false,
+ canCreate: false,
+ canDelete: false,
+ canMove: false,
+ canPublish: false,
+ canUnpublish: false
};
$scope.$watch("selection.length", function (newVal, oldVal) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html
index c25b1d506a..a5757860c9 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html
@@ -2,13 +2,14 @@
-
@@ -44,7 +45,7 @@
-
-
-
-
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 0e61b4b5fa..67488c55dd 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
@@ -29,7 +29,7 @@
}
});
- function MediaPicker3Controller($scope, editorService, clipboardService, localizationService, overlayService, userService, entityResource, $attrs, umbRequestHelper, $injector, uploadTracker) {
+ function MediaPicker3Controller($scope, $element, editorService, clipboardService, localizationService, overlayService, userService, entityResource, $attrs, umbRequestHelper, $injector, uploadTracker, editorState) {
const mediaUploader = $injector.instantiate(Utilities.MediaUploader);
let uploadInProgress = false;
@@ -50,6 +50,7 @@
vm.allowAddMedia = true;
vm.allowRemoveMedia = true;
vm.allowEditMedia = true;
+ vm.allowDropMedia = true;
vm.handleFiles = handleFiles;
@@ -74,11 +75,20 @@
vm.allowAddMedia = !vm.readonly;
vm.allowRemoveMedia = !vm.readonly;
vm.allowEditMedia = !vm.readonly;
+ vm.allowDropMedia = !vm.readonly;
vm.sortableOptions.disabled = vm.readonly;
});
vm.$onInit = function() {
+ vm.node = vm.node || editorState.getCurrent();
+
+ // If we do not have a node on the scope, then disallow drop media
+ if (!vm.node?.key) {
+ console.warn('An Umbraco.MediaPicker3 did not detect a valid content node and disabled drag & drop.', $element[0]);
+ vm.allowDropMedia = false;
+ }
+
vm.validationLimit = vm.model.config.validationLimit || {};
// If single-mode we only allow 1 item as the maximum:
if(vm.model.config.multiple === false) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html
index 21e9065d92..d81b858002 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.html
@@ -3,7 +3,7 @@
-
+
diff --git a/src/Umbraco.Web.UI.Docs/gulpfile.js b/src/Umbraco.Web.UI.Docs/gulpfile.js
index 2117b7a176..0d058d684c 100644
--- a/src/Umbraco.Web.UI.Docs/gulpfile.js
+++ b/src/Umbraco.Web.UI.Docs/gulpfile.js
@@ -18,7 +18,7 @@ gulp.task('docs', [], function (cb) {
var options = {
html5Mode: false,
startPage: '/api',
- title: "Umbraco 10 Backoffice UI API Documentation",
+ title: "Umbraco 11 Backoffice UI API Documentation",
dest: './api',
styles: ['./umb-docs.css'],
image: "https://our.umbraco.com/assets/images/logo.svg"
diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
index 1bd05a424d..12c47c5fa4 100644
--- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
@@ -55,7 +56,8 @@ public static partial class UmbracoBuilderExtensions
x.GetRequiredService(),
x.GetRequiredService(),
x.GetRequiredService(),
- x.GetRequiredService()
+ x.GetRequiredService(),
+ x.GetRequiredService>()
));
builder.Services.AddSingleton();
builder.Services.TryAddEnumerable(Singleton());
diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
index 5617797ec7..4e4749860c 100644
--- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
+++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
@@ -14,6 +14,7 @@ using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Cms.Web.Common.Security;
@@ -49,6 +50,7 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
private readonly IRuntimeState _runtime;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IUmbracoVirtualPageRoute _umbracoVirtualPageRoute;
+ private GlobalSettings _globalSettings;
[Obsolete("Please use constructor that is not obsolete, instead of this. This will be removed in Umbraco 13.")]
public UmbracoRouteValueTransformer(
@@ -64,7 +66,7 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
IControllerActionSearcher controllerActionSearcher,
IEventAggregator eventAggregator,
IPublicAccessRequestHandler publicAccessRequestHandler)
- : this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler, StaticServiceProvider.Instance.GetRequiredService())
+ : this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService>())
{
}
@@ -79,10 +81,26 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
IPublicAccessRequestHandler publicAccessRequestHandler)
- : this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler, StaticServiceProvider.Instance.GetRequiredService())
+ : this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService>())
{
}
+ [Obsolete("Please use constructor that is not obsolete, instead of this. This will be removed in Umbraco 13.")]
+ public UmbracoRouteValueTransformer(
+ ILogger logger,
+ IUmbracoContextAccessor umbracoContextAccessor,
+ IPublishedRouter publishedRouter,
+ IRuntimeState runtime,
+ IUmbracoRouteValuesFactory routeValuesFactory,
+ IRoutableDocumentFilter routableDocumentFilter,
+ IDataProtectionProvider dataProtectionProvider,
+ IControllerActionSearcher controllerActionSearcher,
+ IPublicAccessRequestHandler publicAccessRequestHandler,
+ IUmbracoVirtualPageRoute umbracoVirtualPageRoute)
+ : this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService>())
+
+ {
+ }
///
/// Initializes a new instance of the class.
@@ -97,7 +115,8 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
IPublicAccessRequestHandler publicAccessRequestHandler,
- IUmbracoVirtualPageRoute umbracoVirtualPageRoute)
+ IUmbracoVirtualPageRoute umbracoVirtualPageRoute,
+ IOptionsMonitor globalSettings)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_umbracoContextAccessor =
@@ -111,6 +130,8 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
_controllerActionSearcher = controllerActionSearcher;
_publicAccessRequestHandler = publicAccessRequestHandler;
_umbracoVirtualPageRoute = umbracoVirtualPageRoute;
+ _globalSettings = globalSettings.CurrentValue;
+ globalSettings.OnChange(x => _globalSettings = x);
}
///
@@ -154,6 +175,17 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
return null!;
}
+ // Check if the maintenance page should be shown
+ if (_runtime.Level == RuntimeLevel.Upgrade && _globalSettings.ShowMaintenancePageWhenInUpgradeState)
+ {
+ return new RouteValueDictionary
+ {
+ // Redirects to the RenderController who handles maintenance page in a filter, instead of having a dedicated controller
+ [ControllerToken] = ControllerExtensions.GetControllerName(),
+ [ActionToken] = nameof(RenderController.Index),
+ };
+ }
+
// Check if there is no existing content and return the no content controller
if (!umbracoContext.Content?.HasContent() ?? false)
{
diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
index a7a1c67434..8fd22e6b0c 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
+++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
@@ -7,8 +7,8 @@
"name": "acceptancetest",
"hasInstallScript": true,
"dependencies": {
- "@umbraco/json-models-builders": "^1.0.2",
- "@umbraco/playwright-testhelpers": "^1.0.20",
+ "@umbraco/json-models-builders": "^1.0.3",
+ "@umbraco/playwright-testhelpers": "^1.0.21",
"camelize": "^1.0.0",
"dotenv": "^16.0.2",
"faker": "^4.1.0",
@@ -129,18 +129,18 @@
"dev": true
},
"node_modules/@umbraco/json-models-builders": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.2.tgz",
- "integrity": "sha512-IxKvdTPHe4O5YB+gCmi9G31ytcUaWyQx5/xWxJOeaD+4/p/xyTOFLBdq1rXme645i2ef3QtWxlCnGK/j15w2dQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.3.tgz",
+ "integrity": "sha512-NNBIP9ZXXZvxanmG5OvE+Ppc2ObSLLUyBbwZiPtwerFxdlnYuUYA6qCq6mj7vx3na6MOQTPZMAiNFEaM0V9xFw==",
"dependencies": {
"camelize": "^1.0.0",
"faker": "^4.1.0"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
- "version": "1.0.20",
- "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.20.tgz",
- "integrity": "sha512-V+0ZmaFmvWicHaZFkGako8FjDA6UvwOkKpHwCIy0xLAEuhLLTEb6yCH61WydAn2BqY7Ft0xUPMvbKMURYddkjA==",
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.21.tgz",
+ "integrity": "sha512-JfE1MvKc7LEVayF9AX4Ctmx8c6+M+m6+mV7g7QSOYPO7ky/PSlzVvI9S9S7vcpuwLB2Vp4NE9PcXaXUGEvNCnw==",
"dependencies": {
"@umbraco/json-models-builders": "^1.0.2",
"camelize": "^1.0.0",
@@ -1055,18 +1055,18 @@
"dev": true
},
"@umbraco/json-models-builders": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.2.tgz",
- "integrity": "sha512-IxKvdTPHe4O5YB+gCmi9G31ytcUaWyQx5/xWxJOeaD+4/p/xyTOFLBdq1rXme645i2ef3QtWxlCnGK/j15w2dQ==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-1.0.3.tgz",
+ "integrity": "sha512-NNBIP9ZXXZvxanmG5OvE+Ppc2ObSLLUyBbwZiPtwerFxdlnYuUYA6qCq6mj7vx3na6MOQTPZMAiNFEaM0V9xFw==",
"requires": {
"camelize": "^1.0.0",
"faker": "^4.1.0"
}
},
"@umbraco/playwright-testhelpers": {
- "version": "1.0.20",
- "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.20.tgz",
- "integrity": "sha512-V+0ZmaFmvWicHaZFkGako8FjDA6UvwOkKpHwCIy0xLAEuhLLTEb6yCH61WydAn2BqY7Ft0xUPMvbKMURYddkjA==",
+ "version": "1.0.21",
+ "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.21.tgz",
+ "integrity": "sha512-JfE1MvKc7LEVayF9AX4Ctmx8c6+M+m6+mV7g7QSOYPO7ky/PSlzVvI9S9S7vcpuwLB2Vp4NE9PcXaXUGEvNCnw==",
"requires": {
"@umbraco/json-models-builders": "^1.0.2",
"camelize": "^1.0.0",
diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json
index 6589bea5b9..abc16a6865 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/package.json
+++ b/tests/Umbraco.Tests.AcceptanceTest/package.json
@@ -19,8 +19,8 @@
"wait-on": "^6.0.1"
},
"dependencies": {
- "@umbraco/json-models-builders": "^1.0.2",
- "@umbraco/playwright-testhelpers": "^1.0.20",
+ "@umbraco/json-models-builders": "^1.0.3",
+ "@umbraco/playwright-testhelpers": "^1.0.21",
"camelize": "^1.0.0",
"faker": "^4.1.0",
"form-data": "^4.0.0",
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts
new file mode 100644
index 0000000000..36e7fd6d00
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeBlocks.spec.ts
@@ -0,0 +1,714 @@
+import {AliasHelper, ConstantHelper, test} from "@umbraco/playwright-testhelpers";
+import {BlockGridDataTypeBuilder} from "@umbraco/json-models-builders/dist/lib/builders/dataTypes";
+import {expect} from "@playwright/test";
+
+test.describe('BlockGridEditorDataTypeBlock', () => {
+ const blockGridName = 'BlockGridEditorTest';
+ const elementName = 'TestElement';
+ const elementAlias = AliasHelper.toAlias(elementName);
+ const elementNameTwo = 'SecondElement';
+ const elementTwoAlias = AliasHelper.toAlias(elementNameTwo);
+ const elementNameThree = 'ThirdElement';
+ const elementThreeAlias = AliasHelper.toAlias(elementNameThree);
+
+ test.beforeEach(async ({page, umbracoApi}, testInfo) => {
+ await umbracoApi.report.report(testInfo);
+ await umbracoApi.login();
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridName);
+ });
+
+ test.afterEach(async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridName);
+ });
+
+ async function createDefaultBlockGridWithElement(umbracoApi) {
+ const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlock()
+ .withContentElementTypeKey(element['key'])
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ return element;
+ }
+
+ async function createEmptyBlockGridWithName(umbracoApi) {
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ return blockGridType;
+ }
+
+ test('can create empty block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoUi.goToSection(ConstantHelper.sections.settings);
+
+ // Creates a new datatype
+ await umbracoUi.clickDataElementByElementName('tree-item-dataTypes', {button: 'right'});
+ await umbracoUi.clickDataElementByElementName(ConstantHelper.actions.create);
+ await umbracoUi.clickDataElementByElementName(ConstantHelper.actions.dataType);
+
+ await umbracoUi.setEditorHeaderName(blockGridName);
+
+ // Adds BlockGrid as property editor
+ await umbracoUi.clickDataElementByElementName('property-editor-add');
+ await umbracoUi.clickDataElementByElementName('propertyeditor-', {hasText: 'Block Grid'});
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the blockGrid dataType was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can create a block grid datatype with an element', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+
+ await createEmptyBlockGridWithName(umbracoApi);
+ await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds an element to the block grid
+ await umbracoUi.clickElement(umbracoUi.getButtonByKey('blockEditor_addBlockType'));
+ await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-' + elementName + '"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submitChanges));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the element is added
+ await expect(page.locator('umb-block-card', {hasText: elementName})).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ });
+
+ test('can create block grid datatype with two elements', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+
+ await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+
+ await createDefaultBlockGridWithElement(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds an element to the block grid
+ await umbracoUi.clickElement(umbracoUi.getButtonByKey('blockEditor_addBlockType'));
+ await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-' + elementNameTwo + '"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submitChanges));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the elements are added
+ await expect(page.locator('umb-block-card', {hasText: elementName})).toBeVisible();
+ await expect(page.locator('umb-block-card', {hasText: elementNameTwo})).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ });
+
+ test('can create a block grid datatype with an element in a group', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+
+ const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Creates the group
+ await umbracoUi.clickElement(umbracoUi.getButtonByKey('blockEditor_addBlockGroup'));
+ await page.locator('[title="group name"]').fill('TestGroup');
+
+ // Adds the element to the created group
+ await page.locator('[key="blockEditor_addBlockType"]').nth(1).click();
+ await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-' + elementName + '"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submitChanges));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the element is added to TestGroup
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + element['key'] + '"]')).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ });
+
+ test('can create a block grid datatype with multiple elements in a group', async ({page, umbracoApi, umbracoUi}) => {
+ const groupOne = 'GroupOne';
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+ const elementThree = await umbracoApi.documentTypes.createDefaultElementType(elementNameThree, elementThreeAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(groupOne)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .withGroupName(groupOne)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .withGroupName(groupOne)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds the element to the created group
+ await page.locator('[key="blockEditor_addBlockType"]').nth(1).click();
+ await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-' + elementNameThree + '"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submitChanges));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the elements are added to GroupOne
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementOne['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementTwo['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementThree['key'] + '"]')).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+ });
+
+ test('can create a block grid datatype with multiple groups with an element in each', async ({page, umbracoApi, umbracoUi}) => {
+ const groupOne = 'GroupOne';
+ const groupTwo = 'GroupTwo';
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+ const elementThree = await umbracoApi.documentTypes.createDefaultElementType(elementNameThree, elementThreeAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(groupOne)
+ .done()
+ .addBlockGroups()
+ .withName(groupTwo)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .withGroupName(groupOne)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds another element to GroupTwo
+ // We need to have a nth because all the add block groups are the same in the html
+ await page.locator('[key="blockEditor_addBlockType"]').nth(2).click();
+ await umbracoUi.clickDataElementByElementName("tree-item-" + elementNameThree);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submitChanges));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the elements is are added to their correct groups
+ await expect(page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + elementOne['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementTwo['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementThree['key'] + '"]')).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+ });
+
+ test('can create a block grid datatype with multiple groups and multiple element in each group', async ({page, umbracoApi, umbracoUi}) => {
+ const GroupOne = 'GroupOne';
+ const elementNameFourth = 'FourthElement';
+ const elementFourthAlias = AliasHelper.toAlias(elementNameFourth);
+ const elementNameFifth = 'FifthElement';
+ const elementFifthAlias = AliasHelper.toAlias(elementNameFifth);
+ const elementNameSixth = 'SixthElement';
+ const elementSixthAlias = AliasHelper.toAlias(elementNameSixth);
+
+ const GroupTwo = 'GroupTwo';
+ const elementNameSeventh = 'SeventhElement';
+ const elementSeventhAlias = AliasHelper.toAlias(elementNameSeventh);
+ const elementNameEighth = 'EightElement';
+ const elementEighthAlias = AliasHelper.toAlias(elementNameEighth);
+ const elementNameNinth = 'NinthElement';
+ const elementNinthAlias = AliasHelper.toAlias(elementNameNinth);
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameFourth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameFifth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameSixth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameSeventh);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameEighth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameNinth);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+ const elementThree = await umbracoApi.documentTypes.createDefaultElementType(elementNameThree, elementThreeAlias);
+ const elementFour = await umbracoApi.documentTypes.createDefaultElementType(elementNameFourth, elementFourthAlias);
+ const elementFive = await umbracoApi.documentTypes.createDefaultElementType(elementNameFifth, elementFifthAlias);
+ const elementSix = await umbracoApi.documentTypes.createDefaultElementType(elementNameSixth, elementSixthAlias);
+ const elementSeven = await umbracoApi.documentTypes.createDefaultElementType(elementNameSeventh, elementSeventhAlias);
+ const elementEight = await umbracoApi.documentTypes.createDefaultElementType(elementNameEighth, elementEighthAlias);
+ const elementNine = await umbracoApi.documentTypes.createDefaultElementType(elementNameNinth, elementNinthAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(GroupOne)
+ .done()
+ .addBlockGroups()
+ .withName(GroupTwo)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementThree['key'])
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementFour['key'])
+ .withGroupName(GroupOne)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementFive['key'])
+ .withGroupName(GroupOne)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementSix['key'])
+ .withGroupName(GroupOne)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementSeven['key'])
+ .withGroupName(GroupTwo)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementEight['key'])
+ .withGroupName(GroupTwo)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds the ninth element to GroupTwo
+ await page.locator('[key="blockEditor_addBlockType"]').nth(2).click();
+ await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-' + elementNameNinth + '"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submitChanges));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the elements is are added to their correct groups
+ await expect(page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + elementOne['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + elementTwo['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + elementThree['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementFour['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementFive['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementSix['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementSeven['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementEight['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementNine['key'] + '"]')).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameFourth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameFifth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameSixth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameSeventh);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameEighth);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameNinth);
+ });
+
+ test('cant add an element which already exists in a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+
+ const element = await createDefaultBlockGridWithElement(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Tries to add the same element to the block grid
+ await umbracoUi.clickElement(umbracoUi.getButtonByKey('blockEditor_addBlockType'));
+ await page.locator('[data-element="editor-container"]').locator('[data-element="tree-item-' + elementName + '"]').click();
+
+ // Assert
+ await expect(page.locator('.not-allowed', {hasText: elementName})).toBeVisible();
+ // Checks if the button create New Element Type is still visible. If visible the element was not clickable.
+ await expect(page.locator('[label-key="blockEditor_labelcreateNewElementType"]')).toBeVisible();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.close));
+ await expect(page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + element['key'] + '"]')).toHaveCount(1);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ });
+
+ test('can remove an element from a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+
+ const element = await createDefaultBlockGridWithElement(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Removes the element
+ await page.locator('[data-content-element-type-key="' + element['key'] + '"]').locator('.btn-reset').click();
+ // Cant use the constant key because the constant key is "action-delete"
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey("actions_delete"));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks to make sure the element is removed
+ await expect(page.locator('[data-content-element-type-key="' + element['key'] + '"]')).not.toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ });
+
+ test('can delete a group without elements from a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const Group = "GroupToBeDeleted";
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(Group)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Removes the empty group
+ await page.locator('.umb-block-card-group').nth(1).locator('[title="Delete"]').click();
+ // Cant use the constant key because the correct constant key is "action-delete"
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey("actions_delete"));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks to make sure the element is removed
+ await expect(page.locator('.umb-block-card-group').nth(1)).not.toBeVisible();
+ // Checks if the datatype exists
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can delete a group with elements from a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+
+ const Group = "GroupToBeDeleted";
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(Group)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .withGroupName(Group)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .withGroupName(Group)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Removes the group with elements
+ await page.locator('.umb-block-card-group').nth(1).locator('[title="Delete"]').click();
+ // Cant use the constant key because the correct constant key is "action-delete"
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey("actions_delete"));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks to make sure the element is removed
+ await expect(page.locator('.umb-block-card-group').nth(1)).not.toBeVisible();
+ await expect(page.locator('[data-content-element-type-key="' + elementOne['key'] + '"]')).not.toBeVisible();
+ await expect(page.locator('[data-content-element-type-key="' + elementTwo['key'] + '"]')).not.toBeVisible();
+ // Checks if the datatype exists
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ });
+
+ test('can delete an empty block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const blockGridType = await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.goToSection(ConstantHelper.sections.settings);
+
+ // Deletes the empty block grid editor
+ await page.locator('[data-element="tree-item-dataTypes"]').locator('[data-element="tree-item-expand"]').click();
+ await umbracoUi.clickDataElementByElementName("tree-item-" + blockGridName, {button: 'right'});
+ await umbracoUi.clickDataElementByElementName(ConstantHelper.actions.delete);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.delete));
+
+ // Assert
+ // Checks if the block grid editor still exists
+ await umbracoUi.goToSection(ConstantHelper.sections.settings);
+ await umbracoUi.clickDataElementByElementName('tree-item-dataTypes', {button: "right"});
+ await umbracoUi.clickDataElementByElementName('action-refreshNode');
+ await expect(page.locator('[data-element="tree-item-dataTypes"] >> [data-element="tree-item-' + blockGridType + '"]')).not.toBeVisible();
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(false);
+ });
+
+ test('can delete an block grid editor with elements and groups', async ({page, umbracoApi, umbracoUi}) => {
+ const groupOne = 'GroupOne';
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(groupOne)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .withGroupName(groupOne)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.goToSection(ConstantHelper.sections.settings);
+
+ // Deletes the empty block grid editor
+ await page.locator('[data-element="tree-item-dataTypes"]').locator('[data-element="tree-item-expand"]').click();
+ await umbracoUi.clickDataElementByElementName("tree-item-" + blockGridName, {button: 'right'});
+ await umbracoUi.clickDataElementByElementName(ConstantHelper.actions.delete);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.delete));
+
+ // Assert
+ // Checks if the block grid editor still exists
+ await umbracoUi.goToSection(ConstantHelper.sections.settings);
+ await umbracoUi.clickDataElementByElementName('tree-item-dataTypes', {button: "right"});
+ await umbracoUi.clickDataElementByElementName('action-refreshNode');
+ await expect(page.locator('[data-element="tree-item-dataTypes"] >> [data-element="tree-item-' + blockGridType + '"]')).not.toBeVisible();
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(false);
+ });
+
+ test('can move an element in a block grid editor to another group', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+
+ const GroupMoveHere = 'MoveToHere';
+
+ const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(GroupMoveHere)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(element['key'])
+ .withLabel('Moved')
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Drags the element from the default group to the 'MoveToHere' Group.
+ await page.locator('.umb-block-card-group').nth(0).locator('[data-content-element-type-key="' + element['key'] + '"]').dragTo(page.locator('[key="blockEditor_addBlockType"]').nth(1));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the elements was moved to the correct group
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + element['key'] + '"]')).toBeVisible();
+ // Checks if the element still contains the correct text
+ await page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + element['key'] + '"]').click();
+ await expect(page.locator('input[name="label"]')).toHaveValue('Moved');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.close));
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ });
+
+ test('can move an empty group in a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const GroupMove = 'GroupMove';
+ const GroupNotMoving = 'GroupNotMoving';
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(GroupMove)
+ .done()
+ .addBlockGroups()
+ .withName(GroupNotMoving)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Drags the group GroupMove under GroupNotMoving
+ await page.locator('.umb-block-card-group >> [icon="icon-navigation"]').nth(0).dragTo(page.locator('[key="blockEditor_addBlockType"]').nth(2));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the groups are in the correct order
+ // The reason that we are checking nth(0) and not 1, is because the default group has no input.
+ await expect(page.locator('.umb-block-card-group >> input[title="group name"]').nth(0)).toHaveValue(GroupNotMoving);
+ await expect(page.locator('.umb-block-card-group >> input[title="group name"]').nth(1)).toHaveValue(GroupMove);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can move a group with elements in a block grid editor ', async ({page, umbracoApi, umbracoUi}) => {
+ const GroupMove = 'GroupMove';
+ const GroupNotMoving = 'GroupNotMoving';
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+ const elementThree = await umbracoApi.documentTypes.createDefaultElementType(elementNameThree, elementThreeAlias);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlockGroups()
+ .withName(GroupMove)
+ .done()
+ .addBlockGroups()
+ .withName(GroupNotMoving)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .withLabel('MovedOne')
+ .withGroupName(GroupMove)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .withLabel('MovedTwo')
+ .withGroupName(GroupMove)
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementThree['key'])
+ .withLabel('MovedThree')
+ .withGroupName(GroupNotMoving)
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Drags the group GroupMove under GroupNotMoving
+ await page.locator('.umb-block-card-group >> [icon="icon-navigation"]').nth(0).hover();
+ await page.mouse.down();
+ await page.mouse.move(0, -20);
+ await page.locator('[key="blockEditor_addBlockType"]').nth(2).hover({
+ position: {
+ x: 0, y: 20
+ }
+ });
+ await page.mouse.up();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the elements were moved with their group
+ await expect(page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementThree['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementOne['key'] + '"]')).toBeVisible();
+ await expect(page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementTwo['key'] + '"]')).toBeVisible();
+ // Checks if the moved elements still contains the correct text
+ // ElementThree in GroupNotMoving
+ await page.locator('.umb-block-card-group').nth(1).locator('[data-content-element-type-key="' + elementThree['key'] + '"]').click();
+ await expect(page.locator('input[name="label"]')).toHaveValue('MovedThree');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.close));
+ // ElementOne in GroupMove
+ await page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementOne['key'] + '"]').click();
+ await expect(page.locator('input[name="label"]')).toHaveValue('MovedOne');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.close));
+ // ElementTwo in GroupMove
+ await page.locator('.umb-block-card-group').nth(2).locator('[data-content-element-type-key="' + elementTwo['key'] + '"]').click();
+ await expect(page.locator('input[name="label"]')).toHaveValue('MovedTwo');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.close));
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameThree);
+ });
+});
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeConfiguration.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeConfiguration.spec.ts
new file mode 100644
index 0000000000..5d3577b5ba
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Datatype/BlockGridEditorDataTypeConfiguration.spec.ts
@@ -0,0 +1,326 @@
+import {ConstantHelper, test} from "@umbraco/playwright-testhelpers";
+import {expect} from "@playwright/test";
+import {BlockGridDataTypeBuilder} from "@umbraco/json-models-builders/dist/lib/builders/dataTypes";
+import {StylesheetBuilder} from "@umbraco/json-models-builders";
+
+test.describe('BlockGridEditorDataTypeConfiguration', () => {
+ const blockGridName = 'BlockGridEditorTest';
+
+ test.beforeEach(async ({page, umbracoApi}, testInfo) => {
+ await umbracoApi.report.report(testInfo);
+ await umbracoApi.login();
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridName);
+ });
+
+ test.afterEach(async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridName);
+ });
+
+ async function createEmptyBlockGridWithName(umbracoApi) {
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ return blockGridType;
+ }
+
+ test.describe('Amount tests', () => {
+
+ test('can add a min and max amount to a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Changes the amount in min and max
+ await page.locator('[name="numberFieldMin"]').fill('2');
+ await page.locator('[name="numberFieldMax"]').fill('4');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if min and max were added
+ await expect(page.locator('input[name="numberFieldMin"]')).toHaveValue('2');
+ await expect(page.locator('input[name="numberFieldMax"]')).toHaveValue('4');
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can edit a min and max amount in a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .withMin(1)
+ .withMax(2)
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Updates min so it's equal to max
+ await page.locator('[name="numberFieldMin"]').fill('2');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if min and max were updated
+ await expect(page.locator('input[name="numberFieldMin"]')).toHaveValue('2');
+ await expect(page.locator('input[name="numberFieldMax"]')).toHaveValue('2');
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('min amount cant be more than max amount in a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Updates min so it's more than max
+ await page.locator('[name="numberFieldMin"]').fill('4');
+ await page.locator('[name="numberFieldMax"]').fill('2');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ // Checks if an error message is visible.
+ await expect(page.locator('.alert-error >> "This property is invalid"')).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+ });
+
+ test.describe('Live editing mode tests', () => {
+
+ test('can turn live editing mode on for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Enables live editing mode
+ await page.locator('[id="useLiveEditing"]').click();
+
+ // Assert
+ // Checks if live editing mode is true
+ await expect(page.locator('.umb-property-editor >> .umb-toggle--checked')).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+ });
+
+ test.describe('Editor width tests', () => {
+
+ test('can add editor width for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const editorWidth = '100%';
+
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds editor width
+ await page.locator('[id="maxPropertyWidth"]').fill(editorWidth);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the editor width was added
+ await expect(page.locator('input[id="maxPropertyWidth"]')).toHaveValue(editorWidth);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can edit editor width for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const editorWidth = '50%';
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .withMaxPropertyWidth('100%')
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Edits editor width
+ await page.locator('[id="maxPropertyWidth"]').fill(editorWidth);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the editor width was updated
+ await expect(page.locator('input[id="maxPropertyWidth"]')).toHaveValue(editorWidth);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+ });
+
+ test.describe('Grid columns tests', () => {
+
+ test('can add grid columns for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const gridColumns = '10';
+
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds grid columns
+ await page.locator('[name="numberField"]').fill(gridColumns);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the grid columns was added
+ await expect(page.locator('input[name="numberField"]')).toHaveValue(gridColumns);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can edit grid columns for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const gridColumns = '9';
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .withGridColumns(8)
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Edits grid columns
+ await page.locator('[name="numberField"]').fill(gridColumns);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if grid columns were updated
+ await expect(page.locator('input[name="numberField"]')).toHaveValue(gridColumns);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+ });
+
+ test.describe('Layout stylesheet tests', () => {
+
+ test('can add a layout stylesheet for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const stylesheetName = 'StylesheetTest';
+
+ await umbracoApi.stylesheets.ensureNameNotExists(stylesheetName + '.css');
+
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ const stylesheet = new StylesheetBuilder()
+ .withVirtualPath("/css/")
+ .withFileType("stylesheets")
+ .withName(stylesheetName)
+ .build();
+ await umbracoApi.stylesheets.save(stylesheet);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ await umbracoUi.clickElement(umbracoUi.getButtonByKey('blockEditor_addCustomStylesheet'));
+ await page.locator('[data-element="tree-item-wwwroot"]').locator('[data-element="tree-item-expand"]').click();
+ await page.locator('[data-element="tree-item-css"]').locator('[data-element="tree-item-expand"]').click();
+ await umbracoUi.clickDataElementByElementName('tree-item-' + stylesheetName + '.css');
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the stylesheet was added
+ await expect(page.locator('.umb-node-preview__name', {hasText: 'StylesheetTest'})).toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.stylesheets.ensureNameNotExists(stylesheetName + '.css');
+ });
+
+ test('can remove a layout stylesheet from a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const stylesheetName = 'StylesheetTest';
+ const path = '/css/';
+
+ await umbracoApi.stylesheets.ensureNameNotExists(stylesheetName + '.css');
+
+ const stylesheet = new StylesheetBuilder()
+ .withVirtualPath(path)
+ .withFileType("stylesheets")
+ .withName(stylesheetName)
+ .build();
+ await umbracoApi.stylesheets.save(stylesheet);
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .withLayoutStylesheet('~' + path + stylesheetName + '.css')
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Removes the stylesheet
+ await page.locator('.__control-actions >> .btn-reset').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the stylesheet is deleted
+ await expect(page.locator('.umb-node-preview__name', {hasText: 'StylesheetTest'})).not.toBeVisible();
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+
+ // Clean
+ await umbracoApi.stylesheets.ensureNameNotExists(stylesheetName + '.css');
+ });
+ });
+
+ test.describe('Create button label tests', () => {
+
+ test('can add a create button label for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const createButtonLabel = 'testButton';
+
+ await createEmptyBlockGridWithName(umbracoApi);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Adds create button label text
+ await page.locator('[id="createLabel"]').fill(createButtonLabel);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the label for create button label was added
+ await expect(page.locator('input[id="createLabel"]')).toHaveValue(createButtonLabel);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+
+ test('can edit a create button label for a block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const editButtonLabel = 'NewButtonLabel';
+
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .withCreateLabel('OldLabel')
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ await umbracoUi.navigateToDataType(blockGridName);
+
+ // Edits create button label text
+ await page.locator('[id="createLabel"]').fill(editButtonLabel);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await umbracoUi.isSuccessNotificationVisible();
+ // Checks if the label for create button label was updated
+ await expect(page.locator('input[id="createLabel"]')).toHaveValue(editButtonLabel);
+ // Checks if the datatype was created
+ await expect(await umbracoApi.dataTypes.exists(blockGridName)).toBe(true);
+ await umbracoUi.doesDataTypeExist(blockGridName);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Document/BlockGridEditorInDocument.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Document/BlockGridEditorInDocument.spec.ts
new file mode 100644
index 0000000000..b5aa9d2119
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/BlockGridEditor/Document/BlockGridEditorInDocument.spec.ts
@@ -0,0 +1,272 @@
+import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers';
+import {DocumentTypeBuilder} from "@umbraco/json-models-builders";
+import {BlockGridDataTypeBuilder} from "@umbraco/json-models-builders/dist/lib/builders/dataTypes";
+import {expect} from "@playwright/test";
+
+test.describe('BlockGridEditorInDocument', () => {
+ const documentName = 'DocumentName';
+ const elementName = 'TestElement';
+ let blockGridName = 'BlockGridTest';
+ const documentGroupName = 'blockGridGroup';
+
+ const blockGridAlias = AliasHelper.toAlias(blockGridName);
+ const elementAlias = AliasHelper.toAlias(elementName);
+
+ test.beforeEach(async ({page, umbracoApi, umbracoUi}, testInfo) => {
+ await umbracoApi.report.report(testInfo);
+ await umbracoApi.login();
+ await umbracoApi.documentTypes.ensureNameNotExists(documentName);
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridName);
+ });
+
+ test.afterEach(async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(documentName);
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridName);
+ });
+
+ async function createDefaultBlockGridEditorWithoutElement(umbracoApi, element, BlockGridName) {
+ const blockGridOne = new BlockGridDataTypeBuilder()
+ .withName(BlockGridName)
+ .addBlock()
+ .withContentElementTypeKey(element['key'])
+ .done()
+ .build();
+ return await umbracoApi.dataTypes.save(blockGridOne);
+ }
+
+ async function createDefaultDocumentTypeWithBlockGridEditor(umbracoApi, BlockGridEditor?) {
+
+ const rootDocType = new DocumentTypeBuilder()
+ .withName(documentName)
+ .withAllowAsRoot(true)
+ .addGroup()
+ .withName(documentGroupName)
+ .addCustomProperty(BlockGridEditor['id'])
+ .withLabel(blockGridName)
+ .withAlias(blockGridAlias)
+ .done()
+ .done()
+ .build();
+ return await umbracoApi.documentTypes.save(rootDocType);
+ }
+
+ test('can create an empty block grid editor in a document', async ({page, umbracoApi, umbracoUi}) => {
+ // Creates a empty document
+ const rootDocType = new DocumentTypeBuilder()
+ .withName(documentName)
+ .withAllowAsRoot(true)
+ .build();
+ await umbracoApi.documentTypes.save(rootDocType);
+
+ await umbracoUi.navigateToDocumentType(documentName);
+
+ // Adds a group with a BlockList editor
+ await umbracoUi.goToAddEditor(documentGroupName, blockGridName);
+ // Waits until the selector is visible
+ await expect(page.locator('[data-element="datatype-Block Grid"]')).toBeVisible();
+ await umbracoUi.clickDataElementByElementName('datatype-Block Grid');
+
+ // Creates a new BlockGridEditor
+ await page.locator('[title="Create a new configuration of Block Grid"]').click();
+ await page.locator('[id="dataTypeName"]').fill(blockGridName);
+ await page.locator('[data-element="editor-data-type-settings"]').locator('[label-key=' + ConstantHelper.buttons.submit + ']').click();
+ // Checks to be sure that the clicked button is not visible
+ await expect(page.locator('[data-element="editor-data-type-settings"]').locator('[label-key=' + ConstantHelper.buttons.submit + ']')).not.toBeVisible();
+ // Checks to ensure that the button is visible
+ await expect(page.locator('[name="propertySettingsForm"]').locator('[label-key=' + ConstantHelper.buttons.submit + ']')).toBeVisible();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Datatype saved"})).toBeVisible();
+ await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible();
+ // Checks if the BlockGridEditor is in the document
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"]', {hasText: blockGridName})).toBeVisible();
+ });
+
+ test('can add a block grid editor with two elements to a document', async ({page, umbracoApi, umbracoUi}) => {
+ const elementNameTwo = 'SecondElement';
+ const elementTwoAlias = AliasHelper.toAlias(elementNameTwo);
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+
+ const elementOne = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementTwoAlias);
+
+ // Creates a BlockGridEditor with two elements
+ const blockGridType = new BlockGridDataTypeBuilder()
+ .withName(blockGridName)
+ .addBlock()
+ .withContentElementTypeKey(elementOne['key'])
+ .done()
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridType);
+
+ // Creates empty document
+ const rootDocType = new DocumentTypeBuilder()
+ .withName(documentName)
+ .withAllowAsRoot(true)
+ .build();
+ await umbracoApi.documentTypes.save(rootDocType);
+
+ await umbracoUi.navigateToDocumentType(documentName);
+
+ // Adds a group with a BlockGridEditor
+ await umbracoUi.goToAddEditor(documentGroupName, blockGridName);
+ // Waits until the selector is visible
+ await expect(page.locator('[data-element="datatype-Block Grid"]')).toBeVisible();
+ await umbracoUi.clickDataElementByElementName('datatype-Block Grid');
+ await page.locator('[title="Select ' + blockGridName + '"]').click();
+ // Checks to be sure that the clicked button is not visible
+ await expect(page.locator('[data-element="editor-data-type-settings"]').locator('[label-key=' + ConstantHelper.buttons.submit + ']')).not.toBeVisible();
+ // Checks to ensure that the button is visible
+ await expect(page.locator('[name="propertySettingsForm"]').locator('[label-key=' + ConstantHelper.buttons.submit + ']')).toBeVisible();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible();
+ // Checks if the BlockGridEditor is in the document
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"]', {hasText: blockGridName})).toBeVisible();
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ });
+
+ test('can create multiple block list editors in a document', async ({page, umbracoApi, umbracoUi}) => {
+ const elementNameTwo = 'ElementNameTwo';
+ const elementAliasTwo = AliasHelper.toAlias(elementNameTwo);
+ const blockGridNameTwo = 'BlockGridNameTwo';
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridNameTwo);
+
+ // Creates the first BlockGridEditor with an Element
+ const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const BlockGridEditorOne = await createDefaultBlockGridEditorWithoutElement(umbracoApi, element, blockGridName);
+
+ // Creates the second BlockGridEditor with an Element
+ // Element
+ const elementTwo = new DocumentTypeBuilder()
+ .withName(elementNameTwo)
+ .withAlias(elementAliasTwo)
+ .AsElementType()
+ .addGroup()
+ .withName('TestString')
+ .withAlias('testString')
+ .addTextBoxProperty()
+ .withLabel('Title')
+ .withAlias('title')
+ .done()
+ .done()
+ .build();
+ await umbracoApi.documentTypes.save(elementTwo);
+ // BlockGrid
+ const blockGridTwo = new BlockGridDataTypeBuilder()
+ .withName(blockGridNameTwo)
+ .addBlock()
+ .withContentElementTypeKey(elementTwo['key'])
+ .withLabel('Howdy')
+ .done()
+ .build();
+ await umbracoApi.dataTypes.save(blockGridTwo);
+
+ // Creates a Document with the first BlockGridEditor
+ await createDefaultDocumentTypeWithBlockGridEditor(umbracoApi, BlockGridEditorOne);
+
+ await umbracoUi.navigateToDocumentType(documentName);
+
+ // Adds another BlockGridEditor
+ await page.locator('[data-element="group-' + documentGroupName + '"]').locator('[data-element="property-add"]').click();
+ await page.locator('[data-element="property-name"]').fill('TheBlock');
+ await umbracoUi.clickDataElementByElementName('editor-add');
+ await expect(page.locator('[data-element="datatype-Block Grid"]')).toBeVisible();
+ await umbracoUi.clickDataElementByElementName('datatype-Block Grid');
+ await page.locator('[title="Select ' + blockGridNameTwo + '"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible();
+ // Checks if the new block list is in the group
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"]', {hasText: blockGridName})).toBeVisible();
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"]', {hasText: blockGridNameTwo})).toBeVisible();
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridNameTwo);
+ });
+
+ test('can change a block grid editor in a document to another block grid editor', async ({page, umbracoApi, umbracoUi}) => {
+ const blockGridNameTwo = 'BlockGridNameTwo';
+ const elementNameTwo = 'ElementNameTwo';
+ const elementAliasTwo = AliasHelper.toAlias(elementNameTwo);
+
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridNameTwo);
+
+ // Creates the first BlockGridEditor
+ const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const BlockGridEditorOne = await createDefaultBlockGridEditorWithoutElement(umbracoApi, element, blockGridName);
+ // Creates the second BlockGridEditor
+ const elementTwo = await umbracoApi.documentTypes.createDefaultElementType(elementNameTwo, elementAliasTwo);
+ await createDefaultBlockGridEditorWithoutElement(umbracoApi, elementTwo, blockGridNameTwo);
+ // Creates Document with the first BlockGridEditor
+ await createDefaultDocumentTypeWithBlockGridEditor(umbracoApi, BlockGridEditorOne);
+
+ await umbracoUi.navigateToDocumentType(documentName);
+
+ // Switches from the first BlockGridBuilder to the second
+ await page.locator('[data-element="group-' + documentGroupName + '"] >> [data-element="property-' + blockGridAlias + '"] >> [title="Edit"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByKey('general_change'));
+ await umbracoUi.clickDataElementByElementName('datatype-Block Grid');
+ await umbracoUi.clickDataElementByElementName('datatypeconfig-' + blockGridNameTwo);
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible();
+ // Checks if the second BlockGridEditor is visible
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"] >> [data-element="property-' + blockGridAlias + '"]', {hasText: blockGridNameTwo})).toBeVisible();
+ // Checks if the first BlockGridEditor is not visible
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"] >> [data-element="property-' + blockGridAlias + '"]', {hasText: blockGridName})).not.toBeVisible();
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ await umbracoApi.documentTypes.ensureNameNotExists(elementNameTwo);
+ await umbracoApi.dataTypes.ensureNameNotExists(blockGridNameTwo);
+ });
+
+ test('can remove a block grid editor from a document', async ({page, umbracoApi, umbracoUi}) => {
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+
+ // Creates the Element and BlockGridEditor
+ const element = await umbracoApi.documentTypes.createDefaultElementType(elementName, elementAlias);
+ const BlockGridEditorOne = await createDefaultBlockGridEditorWithoutElement(umbracoApi, element, blockGridName);
+ // Creates a Document with the BlockGridEditor
+ await createDefaultDocumentTypeWithBlockGridEditor(umbracoApi, BlockGridEditorOne);
+
+ await umbracoUi.navigateToDocumentType(documentName);
+
+ // Deletes the BlockGridEditor from the document
+ await page.locator('[data-element="group-' + documentGroupName + '"] >> [data-element="property-' + blockGridAlias + '"] >> [aria-label="Delete property"]').click();
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey('actions_delete'));
+ await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save));
+
+ // Assert
+ await expect(page.locator('.umb-notifications__notifications > .alert-success', {hasText: "Document Type saved"})).toBeVisible();
+ // Checks if the BlockGridEditor is still in the group
+ await expect(page.locator('[data-element="group-' + documentGroupName + '"] >> [data-element="property-' + blockGridAlias + '"]')).not.toBeVisible();
+
+ // Clean
+ await umbracoApi.documentTypes.ensureNameNotExists(elementName);
+ });
+});
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
index f88c93c2e5..d8f31aa9ed 100644
--- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
+++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
@@ -7,9 +7,9 @@
-
+
-
+
diff --git a/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs b/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs
index 61504bf069..da55c37668 100644
--- a/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs
+++ b/tests/Umbraco.Tests.Common/Builders/LanguageBuilder.cs
@@ -95,6 +95,7 @@ public class LanguageBuilder
return this;
}
+ [Obsolete("This will be replaced in V13 by a corresponding method accepting language ISO code instead of language ID.")]
public LanguageBuilder WithFallbackLanguageIsoCode(string fallbackLanguageIsoCode)
{
_fallbackLanguageIsoCode = fallbackLanguageIsoCode;
diff --git a/tests/Umbraco.Tests.Common/Builders/PartialViewBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PartialViewBuilder.cs
new file mode 100644
index 0000000000..ead5a4f189
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/PartialViewBuilder.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class PartialViewBuilder
+ : BuilderBase
+{
+ private string _path;
+ private string _content;
+ private PartialViewType _viewType = PartialViewType.Unknown;
+
+ public PartialViewBuilder WithPath(string path)
+ {
+ _path = path;
+ return this;
+ }
+
+ public PartialViewBuilder WithContent(string content)
+ {
+ _content = content;
+ return this;
+ }
+
+ public PartialViewBuilder WithViewType(PartialViewType viewType)
+ {
+ _viewType = viewType;
+ return this;
+ }
+
+ public override IPartialView Build()
+ {
+ var path = _path ?? string.Empty;
+ var content = _content ?? string.Empty;
+ var viewType = _viewType;
+
+ return new PartialView(viewType, path) { Content = content };
+ }
+}
diff --git a/tests/Umbraco.Tests.Common/Builders/ScriptBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ScriptBuilder.cs
new file mode 100644
index 0000000000..9e521d0c0f
--- /dev/null
+++ b/tests/Umbraco.Tests.Common/Builders/ScriptBuilder.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Tests.Common.Builders;
+
+public class ScriptBuilder
+ : BuilderBase