Merge branch 'v14/dev' into contrib

This commit is contained in:
Sebastiaan Janssen
2024-08-15 11:10:30 +02:00
51 changed files with 1238 additions and 97 deletions

View File

@@ -39,7 +39,7 @@ public class GetAuditLogDocumentController : DocumentControllerBase
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionProtect.ActionLetter, id),
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)

View File

@@ -66,6 +66,10 @@ public class UpdateDomainsController : DocumentControllerBase
.WithDetail("One or more of the specified domain names were conflicting with domain assignments to other content items.")
.WithExtension("conflictingDomainNames", _domainPresentationFactory.CreateDomainAssignmentModels(result.Result.ConflictingDomains.EmptyNull()))
.Build()),
DomainOperationStatus.InvalidDomainName => BadRequest(problemDetailsBuilder
.WithTitle("Invalid domain name detected")
.WithDetail("One or more of the specified domain names were invalid.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown domain update operation status.")
.Build()),

View File

@@ -98,6 +98,18 @@ public abstract class DocumentTypeControllerBase : ManagementApiControllerBase
.WithTitle("Name was too long")
.WithDetail("Name cannot be more than 255 characters in length.")
.Build()),
ContentTypeOperationStatus.InvalidElementFlagDocumentHasContent => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle("Invalid IsElement flag")
.WithDetail("Cannot change to element type because content has already been created with this document type.")
.Build()),
ContentTypeOperationStatus.InvalidElementFlagElementIsUsedInPropertyEditorConfiguration => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle("Invalid IsElement flag")
.WithDetail("Cannot change to document type because this element type is used in the configuration of a data type.")
.Build()),
ContentTypeOperationStatus.InvalidElementFlagComparedToParent => new BadRequestObjectResult(problemDetailsBuilder
.WithTitle("Invalid IsElement flag")
.WithDetail("Can not create a documentType with inheritance composition where the parent and the new type's IsElement flag are different.")
.Build()),
_ => new ObjectResult("Unknown content type operation status") { StatusCode = StatusCodes.Status500InternalServerError },
});

View File

@@ -45275,4 +45275,4 @@
}
}
}
}
}

View File

@@ -398,11 +398,14 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Segments
Services.AddUnique<ISegmentService, NoopSegmentService>();
// definition Import/export
Services.AddUnique<ITemporaryFileToXmlImportService, TemporaryFileToXmlImportService>();
Services.AddUnique<IContentTypeImportService, ContentTypeImportService>();
Services.AddUnique<IMediaTypeImportService, MediaTypeImportService>();
// add validation services
Services.AddUnique<IElementSwitchValidator, ElementSwitchValidator>();
}
}
}

View File

@@ -219,6 +219,24 @@ public static class ObjectExtensions
}
}
if (target == typeof(DateTime) && input is DateTimeOffset dateTimeOffset)
{
// IMPORTANT: for compatability with various editors, we must discard any Offset information and assume UTC time here
return Attempt.Succeed((object?)new DateTime(
new DateOnly(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day),
new TimeOnly(dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Millisecond, dateTimeOffset.Microsecond),
DateTimeKind.Utc));
}
if (target == typeof(DateTimeOffset) && input is DateTime dateTime)
{
// IMPORTANT: for compatability with various editors, we must discard any DateTimeKind information and assume UTC time here
return Attempt.Succeed((object?)new DateTimeOffset(
new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day),
new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond),
TimeSpan.Zero));
}
TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target);
if (inputConverter != null)
{

View File

@@ -0,0 +1,104 @@
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
namespace Umbraco.Cms.Core.Handlers;
public class WarnDocumentTypeElementSwitchNotificationHandler :
INotificationAsyncHandler<ContentTypeSavingNotification>,
INotificationAsyncHandler<ContentTypeSavedNotification>
{
private const string NotificationStateKey =
"Umbraco.Cms.Core.Handlers.WarnDocumentTypeElementSwitchNotificationHandler";
private readonly IEventMessagesFactory _eventMessagesFactory;
private readonly IContentTypeService _contentTypeService;
private readonly IElementSwitchValidator _elementSwitchValidator;
public WarnDocumentTypeElementSwitchNotificationHandler(
IEventMessagesFactory eventMessagesFactory,
IContentTypeService contentTypeService,
IElementSwitchValidator elementSwitchValidator)
{
_eventMessagesFactory = eventMessagesFactory;
_contentTypeService = contentTypeService;
_elementSwitchValidator = elementSwitchValidator;
}
// To figure out whether a warning should be generated, we need both the state before and after saving
public async Task HandleAsync(ContentTypeSavingNotification notification, CancellationToken cancellationToken)
{
IEnumerable<Guid> updatedKeys = notification.SavedEntities
.Where(e => e.HasIdentity)
.Select(e => e.Key);
IEnumerable<IContentType> persistedItems = _contentTypeService.GetAll(updatedKeys);
var stateInformation = persistedItems
.ToDictionary(
contentType => contentType.Key,
contentType => new DocumentTypeElementSwitchInformation { WasElement = contentType.IsElement });
notification.State[NotificationStateKey] = stateInformation;
}
public async Task HandleAsync(ContentTypeSavedNotification notification, CancellationToken cancellationToken)
{
if (notification.State[NotificationStateKey] is not Dictionary<Guid, DocumentTypeElementSwitchInformation>
stateInformation)
{
return;
}
EventMessages eventMessages = _eventMessagesFactory.Get();
foreach (IContentType savedDocumentType in notification.SavedEntities)
{
if (stateInformation.ContainsKey(savedDocumentType.Key) is false)
{
continue;
}
DocumentTypeElementSwitchInformation state = stateInformation[savedDocumentType.Key];
if (state.WasElement == savedDocumentType.IsElement)
{
// no change
continue;
}
await WarnIfAncestorsAreMisaligned(savedDocumentType, eventMessages);
await WarnIfDescendantsAreMisaligned(savedDocumentType, eventMessages);
}
}
private async Task WarnIfAncestorsAreMisaligned(IContentType contentType, EventMessages eventMessages)
{
if (await _elementSwitchValidator.AncestorsAreAlignedAsync(contentType) == false)
{
// todo update this message when the format has been agreed upon on with the client
eventMessages.Add(new EventMessage(
"DocumentType saved",
"One or more ancestors have a mismatching element flag",
EventMessageType.Warning));
}
}
private async Task WarnIfDescendantsAreMisaligned(IContentType contentType, EventMessages eventMessages)
{
if (await _elementSwitchValidator.DescendantsAreAlignedAsync(contentType) == false)
{
// todo update this message when the format has been agreed upon on with the client
eventMessages.Add(new EventMessage(
"DocumentType saved",
"One or more descendants have a mismatching element flag",
EventMessageType.Warning));
}
}
private class DocumentTypeElementSwitchInformation
{
public bool WasElement { get; set; }
}
}

View File

@@ -27,7 +27,6 @@ public interface IDataValueEditor
/// </summary>
bool SupportsReadOnly => false;
/// <summary>
/// Gets the validators to use to validate the edited value.
/// </summary>
@@ -75,4 +74,6 @@ public interface IDataValueEditor
XNode ConvertDbToXml(IPropertyType propertyType, object value);
string ConvertDbToString(IPropertyType propertyType, object? value);
IEnumerable<Guid> ConfiguredElementTypeKeys() => Enumerable.Empty<Guid>();
}

View File

@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public class BlockListConfiguration
{
[ConfigurationField("blocks")]
public BlockConfiguration[] Blocks { get; set; } = null!;
public BlockConfiguration[] Blocks { get; set; } = Array.Empty<BlockConfiguration>();
[ConfigurationField("validationLimit")]
public NumberRange ValidationLimit { get; set; } = new();

View File

@@ -75,6 +75,10 @@ public class DataEditor : IDataEditor
[DataMember(Name = "supportsReadOnly", IsRequired = true)]
public bool SupportsReadOnly { get; set; }
// Adding a virtual method that wraps the default implementation allows derived classes
// to override the default implementation without having to explicitly inherit the interface.
public virtual bool SupportsConfigurableElements => false;
/// <inheritdoc />
[IgnoreDataMember]
public bool IsDeprecated { get; }

View File

@@ -356,6 +356,10 @@ public class DataValueEditor : IDataValueEditor
}
}
// Adding a virtual method that wraps the default implementation allows derived classes
// to override the default implementation without having to explicitly inherit the interface.
public virtual IEnumerable<Guid> ConfiguredElementTypeKeys() => Enumerable.Empty<Guid>();
/// <summary>
/// Used to try to convert the string value to the correct CLR type based on the <see cref="ValueType" /> specified for
/// this value editor.

View File

@@ -16,6 +16,8 @@ public interface IDataEditor : IDiscoverable
bool SupportsReadOnly => false;
bool SupportsConfigurableElements => false;
/// <summary>
/// Gets a value indicating whether the editor is deprecated.
/// </summary>

View File

@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public class RichTextConfiguration : IIgnoreUserStartNodesConfig
{
[ConfigurationField("blocks")]
public RichTextBlockConfiguration[]? Blocks { get; set; } = null!;
public RichTextBlockConfiguration[]? Blocks { get; set; } = Array.Empty<RichTextBlockConfiguration>();
[ConfigurationField("mediaParentId")]
public Guid? MediaParentId { get; set; }

View File

@@ -20,23 +20,19 @@ public class DatePickerValueConverter : PropertyValueConverterBase
internal static DateTime ParseDateTimeValue(object? source)
{
if (source == null)
if (source is null)
{
return DateTime.MinValue;
}
// in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss"
// Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug:
// http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894
// We should just be using TryConvertTo instead.
if (source is string sourceString)
if (source is DateTime dateTimeValue)
{
Attempt<DateTime> attempt = sourceString.TryConvertTo<DateTime>();
return attempt.Success == false ? DateTime.MinValue : attempt.Result;
return dateTimeValue;
}
// in the database a DateTime is: DateTime
// default value is: DateTime.MinValue
return source is DateTime dateTimeValue ? dateTimeValue : DateTime.MinValue;
Attempt<DateTime> attempt = source.TryConvertTo<DateTime>();
return attempt.Success
? attempt.Result
: DateTime.MinValue;
}
}

View File

@@ -24,5 +24,17 @@ public interface IAuthorizationHelper
/// <param name="currentUser">The current user's principal.</param>
/// <param name="user">The resulting <see cref="IUser" />, if the conversion is successful.</param>
/// <returns>True if the conversion is successful, false otherwise</returns>
bool TryGetUmbracoUser(IPrincipal currentUser, [NotNullWhen(true)] out IUser? user);
bool TryGetUmbracoUser(IPrincipal currentUser, [NotNullWhen(true)] out IUser? user)
{
try
{
user = GetUmbracoUser(currentUser);
return true;
}
catch
{
user = null;
return false;
}
}
}

View File

@@ -1,6 +1,9 @@
using Umbraco.Cms.Core.Models;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
@@ -12,8 +15,24 @@ namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<IContentType, IContentTypeService, ContentTypePropertyTypeModel, ContentTypePropertyContainerModel>, IContentTypeEditingService
{
private readonly ITemplateService _templateService;
private readonly IElementSwitchValidator _elementSwitchValidator;
private readonly IContentTypeService _contentTypeService;
public ContentTypeEditingService(
IContentTypeService contentTypeService,
ITemplateService templateService,
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper,
IElementSwitchValidator elementSwitchValidator)
: base(contentTypeService, contentTypeService, dataTypeService, entityService, shortStringHelper)
{
_contentTypeService = contentTypeService;
_templateService = templateService;
_elementSwitchValidator = elementSwitchValidator;
}
[Obsolete("Use the constructor that is not marked obsolete, will be removed in v16")]
public ContentTypeEditingService(
IContentTypeService contentTypeService,
ITemplateService templateService,
@@ -24,6 +43,7 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
{
_contentTypeService = contentTypeService;
_templateService = templateService;
_elementSwitchValidator = StaticServiceProvider.Instance.GetRequiredService<IElementSwitchValidator>();
}
public async Task<Attempt<IContentType?, ContentTypeOperationStatus>> CreateAsync(ContentTypeCreateModel model, Guid userKey)
@@ -52,13 +72,20 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
public async Task<Attempt<IContentType?, ContentTypeOperationStatus>> UpdateAsync(IContentType contentType, ContentTypeUpdateModel model, Guid userKey)
{
Attempt<IContentType?, ContentTypeOperationStatus> result = await ValidateAndMapForUpdateAsync(contentType, model);
if (result.Success is false)
// this needs to happen before the base call as that one is not a pure function
ContentTypeOperationStatus elementValidationStatus = await ValidateElementStatusForUpdateAsync(contentType, model);
if (elementValidationStatus is not ContentTypeOperationStatus.Success)
{
return result;
return Attempt<IContentType?, ContentTypeOperationStatus>.Fail(elementValidationStatus);
}
contentType = result.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result");
Attempt<IContentType?, ContentTypeOperationStatus> baseValidationAttempt = await ValidateAndMapForUpdateAsync(contentType, model);
if (baseValidationAttempt.Success is false)
{
return baseValidationAttempt;
}
contentType = baseValidationAttempt.Result ?? throw new InvalidOperationException($"{nameof(ValidateAndMapForUpdateAsync)} succeeded but did not yield any result");
UpdateHistoryCleanup(contentType, model);
UpdateTemplates(contentType, model);
@@ -77,6 +104,13 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
bool isElement) =>
await FindAvailableCompositionsAsync(key, currentCompositeKeys, currentPropertyAliases, isElement);
protected override async Task<ContentTypeOperationStatus> AdditionalCreateValidationAsync(
ContentTypeEditingModelBase<ContentTypePropertyTypeModel, ContentTypePropertyContainerModel> model)
{
// validate if the parent documentType (if set) has the same element status as the documentType being created
return await ValidateCreateParentElementStatusAsync(model);
}
// update content type history clean-up
private void UpdateHistoryCleanup(IContentType contentType, ContentTypeModelBase model)
{
@@ -100,6 +134,48 @@ internal sealed class ContentTypeEditingService : ContentTypeEditingServiceBase<
contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == model.DefaultTemplateKey));
}
private async Task<ContentTypeOperationStatus> ValidateElementStatusForUpdateAsync(IContentTypeBase contentType, ContentTypeModelBase model)
{
// no change, ignore rest of validation
if (contentType.IsElement == model.IsElement)
{
return ContentTypeOperationStatus.Success;
}
// this method should only contain blocking validation, warnings are handled by WarnDocumentTypeElementSwitchNotificationHandler
// => check whether the element was used in a block structure prior to updating
if (model.IsElement is false)
{
return await _elementSwitchValidator.ElementToDocumentNotUsedInBlockStructuresAsync(contentType)
? ContentTypeOperationStatus.Success
: ContentTypeOperationStatus.InvalidElementFlagElementIsUsedInPropertyEditorConfiguration;
}
return await _elementSwitchValidator.DocumentToElementHasNoContentAsync(contentType)
? ContentTypeOperationStatus.Success
: ContentTypeOperationStatus.InvalidElementFlagDocumentHasContent;
}
/// <summary>
/// Should be called after it has been established that the composition list is in a valid state and the (composition) parent exists
/// </summary>
private async Task<ContentTypeOperationStatus> ValidateCreateParentElementStatusAsync(
ContentTypeEditingModelBase<ContentTypePropertyTypeModel, ContentTypePropertyContainerModel> model)
{
Guid? parentId = model.Compositions
.SingleOrDefault(composition => composition.CompositionType == CompositionType.Inheritance)?.Key;
if (parentId is null)
{
return ContentTypeOperationStatus.Success;
}
IContentType? parent = await _contentTypeService.GetAsync(parentId.Value);
return parent!.IsElement == model.IsElement
? ContentTypeOperationStatus.Success
: ContentTypeOperationStatus.InvalidElementFlagComparedToParent;
}
protected override IContentType CreateContentType(IShortStringHelper shortStringHelper, int parentId)
=> new ContentType(shortStringHelper, parentId);

View File

@@ -91,6 +91,8 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
return Attempt.FailWithStatus<TContentType?, ContentTypeOperationStatus>(operationStatus, null);
}
await AdditionalCreateValidationAsync(model);
// get the ID of the parent to create the content type under (we already validated that it exists)
var parentId = GetParentId(model, containerKey) ?? throw new ArgumentException("Parent ID could not be found", nameof(model));
TContentType contentType = CreateContentType(_shortStringHelper, parentId);
@@ -137,6 +139,10 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
return Attempt.SucceedWithStatus<TContentType?, ContentTypeOperationStatus>(ContentTypeOperationStatus.Success, contentType);
}
protected virtual async Task<ContentTypeOperationStatus> AdditionalCreateValidationAsync(
ContentTypeEditingModelBase<TPropertyTypeModel, TPropertyTypeContainer> model)
=> await Task.FromResult(ContentTypeOperationStatus.Success);
#region Sanitization
private void SanitizeModelAliases(ContentTypeEditingModelBase<TPropertyTypeModel, TPropertyTypeContainer> model)

View File

@@ -0,0 +1,66 @@
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
public class ElementSwitchValidator : IElementSwitchValidator
{
private readonly IContentTypeService _contentTypeService;
private readonly PropertyEditorCollection _propertyEditorCollection;
private readonly IDataTypeService _dataTypeService;
public ElementSwitchValidator(
IContentTypeService contentTypeService,
PropertyEditorCollection propertyEditorCollection,
IDataTypeService dataTypeService)
{
_contentTypeService = contentTypeService;
_propertyEditorCollection = propertyEditorCollection;
_dataTypeService = dataTypeService;
}
public async Task<bool> AncestorsAreAlignedAsync(IContentType contentType)
{
// this call does not return the system roots
var ancestorIds = contentType.AncestorIds();
if (ancestorIds.Length == 0)
{
// if there are no ancestors, validation passes
return true;
}
// if there are any ancestors where IsElement is different from the contentType, the validation fails
return await Task.FromResult(_contentTypeService.GetAll(ancestorIds)
.Any(ancestor => ancestor.IsElement != contentType.IsElement) is false);
}
public async Task<bool> DescendantsAreAlignedAsync(IContentType contentType)
{
IEnumerable<IContentType> descendants = _contentTypeService.GetDescendants(contentType.Id, false);
// if there are any descendants where IsElement is different from the contentType, the validation fails
return await Task.FromResult(descendants.Any(descendant => descendant.IsElement != contentType.IsElement) is false);
}
public async Task<bool> ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType)
{
// get all propertyEditors that support block usage
IDataEditor[] editors = _propertyEditorCollection.Where(pe => pe.SupportsConfigurableElements).ToArray();
var blockEditorAliases = editors.Select(pe => pe.Alias).ToArray();
// get all dataTypes that are based on those propertyEditors
IEnumerable<IDataType> dataTypes = await _dataTypeService.GetByEditorAliasAsync(blockEditorAliases);
// if any dataType has a configuration where this element is selected as a possible block, the validation fails.
return dataTypes.Any(dataType =>
editors.First(editor => editor.Alias == dataType.EditorAlias)
.GetValueEditor(dataType.ConfigurationObject)
.ConfiguredElementTypeKeys().Contains(contentType.Key)) is false;
}
public async Task<bool> DocumentToElementHasNoContentAsync(IContentTypeBase contentType) =>
// if any content for the content type exists, the validation fails.
await Task.FromResult(_contentTypeService.HasContentNodes(contentType.Id) is false);
}

View File

@@ -0,0 +1,14 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
public interface IElementSwitchValidator
{
Task<bool> AncestorsAreAlignedAsync(IContentType contentType);
Task<bool> DescendantsAreAlignedAsync(IContentType contentType);
Task<bool> ElementToDocumentNotUsedInBlockStructuresAsync(IContentTypeBase contentType);
Task<bool> DocumentToElementHasNoContentAsync(IContentTypeBase contentType);
}

View File

@@ -330,6 +330,16 @@ namespace Umbraco.Cms.Core.Services.Implement
return Task.FromResult(dataTypes);
}
/// <inheritdoc />
public async Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias)
{
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
IQuery<IDataType> query = Query<IDataType>().Where(x => propertyEditorAlias.Contains(x.EditorAlias));
IEnumerable<IDataType> dataTypes = _dataTypeRepository.Get(query).ToArray();
ConvertMissingEditorsOfDataTypesToLabels(dataTypes);
return await Task.FromResult(dataTypes);
}
/// <inheritdoc />
public Task<IEnumerable<IDataType>> GetByEditorUiAlias(string editorUiAlias)

View File

@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Extensions;
@@ -201,6 +202,11 @@ public class DomainService : RepositoryService, IDomainService
foreach (DomainModel domainModel in updateModel.Domains)
{
domainModel.DomainName = domainModel.DomainName.ToLowerInvariant();
if(Uri.IsWellFormedUriString(domainModel.DomainName, UriKind.RelativeOrAbsolute) is false)
{
return Attempt.FailWithStatus(DomainOperationStatus.InvalidDomainName, new DomainUpdateResult());
}
}
// make sure we're not attempting to assign duplicate domains

View File

@@ -240,4 +240,11 @@ public interface IDataTypeService : IService
/// <param name="dataType">The data type whose configuration to validate.</param>
/// <returns>One or more <see cref="ValidationResult"/> if the configuration data is invalid, an empty collection otherwise.</returns>
IEnumerable<ValidationResult> ValidateConfigurationData(IDataType dataType);
/// <summary>
/// Gets all <see cref="IDataType" /> for a set of property editors
/// </summary>
/// <param name="propertyEditorAlias">Aliases of the property editors</param>
/// <returns>Collection of <see cref="IDataType" /> configured for the property editors</returns>
Task<IEnumerable<IDataType>> GetByEditorAliasAsync(string[] propertyEditorAlias);
}

View File

@@ -1002,7 +1002,6 @@ namespace Umbraco.Cms.Core.Services
MoveToRecycleBinEventInfo<IMedia>[] moveInfo = moves.Select(x => new MoveToRecycleBinEventInfo<IMedia>(x.Item1, x.Item2)).ToArray();
scope.Notifications.Publish(new MediaMovedToRecycleBinNotification(moveInfo, messages).WithStateFrom(movingToRecycleBinNotification));
Audit(AuditType.Move, userId, media.Id, "Move Media to recycle bin");
scope.Complete();
}

View File

@@ -21,4 +21,7 @@ public enum ContentTypeOperationStatus
NotFound,
NotAllowed,
CancelledByNotification,
InvalidElementFlagDocumentHasContent,
InvalidElementFlagElementIsUsedInPropertyEditorConfiguration,
InvalidElementFlagComparedToParent,
}

View File

@@ -7,5 +7,6 @@ public enum DomainOperationStatus
ContentNotFound,
LanguageNotFound,
DuplicateDomainName,
ConflictingDomainName
ConflictingDomainName,
InvalidDomainName
}

View File

@@ -407,6 +407,11 @@ public static partial class UmbracoBuilderExtensions
builder
.AddNotificationHandler<ContentPublishedNotification, AddDomainWarningsWhenPublishingNotificationHandler>();
// Handlers for save warnings
builder
.AddNotificationAsyncHandler<ContentTypeSavingNotification, WarnDocumentTypeElementSwitchNotificationHandler>()
.AddNotificationAsyncHandler<ContentTypeSavedNotification, WarnDocumentTypeElementSwitchNotificationHandler>();
return builder;
}

View File

@@ -2,12 +2,14 @@
// See LICENSE for more details.
using System.Globalization;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope;
namespace Umbraco.Cms.Core.Events;
@@ -21,21 +23,35 @@ public sealed class RelateOnTrashNotificationHandler :
private readonly IAuditService _auditService;
private readonly IEntityService _entityService;
private readonly IRelationService _relationService;
private readonly IScopeProvider _scopeProvider;
private readonly ICoreScopeProvider _scopeProvider;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly ILocalizedTextService _textService;
[Obsolete("Use the new constructor instead, will be removed in V16")]
public RelateOnTrashNotificationHandler(
IRelationService relationService,
IEntityService entityService,
ILocalizedTextService textService,
IAuditService auditService,
IScopeProvider scopeProvider)
: this(relationService, entityService, textService, auditService, scopeProvider, StaticServiceProvider.Instance.GetRequiredService<IBackOfficeSecurityAccessor>())
{
}
public RelateOnTrashNotificationHandler(
IRelationService relationService,
IEntityService entityService,
ILocalizedTextService textService,
IAuditService auditService,
IScopeProvider scopeProvider,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_relationService = relationService;
_entityService = entityService;
_textService = textService;
_auditService = auditService;
_scopeProvider = scopeProvider;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}
public void Handle(ContentMovedNotification notification)
@@ -56,7 +72,7 @@ public sealed class RelateOnTrashNotificationHandler :
public void Handle(ContentMovedToRecycleBinNotification notification)
{
using (IScope scope = _scopeProvider.CreateScope())
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias;
IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias);
@@ -90,7 +106,7 @@ public sealed class RelateOnTrashNotificationHandler :
_auditService.Add(
AuditType.Delete,
item.Entity.WriterId,
_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? item.Entity.WriterId,
item.Entity.Id,
UmbracoObjectTypes.Document.GetName(),
string.Format(_textService.Localize("recycleBin", "contentTrashed"), item.Entity.Id, originalParentId));
@@ -118,7 +134,7 @@ public sealed class RelateOnTrashNotificationHandler :
public void Handle(MediaMovedToRecycleBinNotification notification)
{
using (IScope scope = _scopeProvider.CreateScope())
using (ICoreScope scope = _scopeProvider.CreateCoreScope())
{
const string relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias;
IRelationType? relationType = _relationService.GetRelationTypeByAlias(relationTypeAlias);
@@ -150,7 +166,7 @@ public sealed class RelateOnTrashNotificationHandler :
_relationService.Save(relation);
_auditService.Add(
AuditType.Delete,
item.Entity.CreatorId,
_backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? item.Entity.WriterId,
item.Entity.Id,
UmbracoObjectTypes.Media.GetName(),
string.Format(_textService.Localize("recycleBin", "mediaTrashed"), item.Entity.Id, originalParentId));

View File

@@ -88,5 +88,8 @@ public class UmbracoPlan : MigrationPlan
// To 14.1.0
To<V_14_1_0.MigrateRichTextConfiguration>("{FEF2DAF4-5408-4636-BB0E-B8798DF8F095}");
To<V_14_1_0.MigrateOldRichTextSeedConfiguration>("{A385C5DF-48DC-46B4-A742-D5BB846483BC}");
// To 14.2.0
To<V_14_2_0.AddMissingDateTimeConfiguration>("{20ED404C-6FF9-4F91-8AC9-2B298E0002EB}");
}
}

View File

@@ -0,0 +1,50 @@
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_14_2_0;
public class AddMissingDateTimeConfiguration : MigrationBase
{
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
public AddMissingDateTimeConfiguration(IMigrationContext context, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
: base(context)
=> _configurationEditorJsonSerializer = configurationEditorJsonSerializer;
protected override void Migrate()
{
Sql<ISqlContext> sql = Sql()
.Select<DataTypeDto>()
.From<DataTypeDto>()
.Where<DataTypeDto>(dto =>
dto.NodeId == Constants.DataTypes.DateTime
&& dto.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DateTime));
DataTypeDto? dataTypeDto = Database.FirstOrDefault<DataTypeDto>(sql);
if (dataTypeDto is null)
{
return;
}
Dictionary<string, object> configurationData = dataTypeDto.Configuration.IsNullOrWhiteSpace()
? new Dictionary<string, object>()
: _configurationEditorJsonSerializer
.Deserialize<Dictionary<string, object?>>(dataTypeDto.Configuration)?
.Where(item => item.Value is not null)
.ToDictionary(item => item.Key, item => item.Value!)
?? new Dictionary<string, object>();
// only proceed with the migration if the data-type has no format assigned
if (configurationData.TryAdd("format", "YYYY-MM-DD HH:mm:ss") is false)
{
return;
}
dataTypeDto.Configuration = _configurationEditorJsonSerializer.Serialize(configurationData);
Database.Update(dataTypeDto);
}
}

View File

@@ -22,6 +22,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory)
=> _ioHelper = ioHelper;
public override bool SupportsConfigurableElements => true;
#region Pre Value Editor

View File

@@ -111,6 +111,12 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
return validationResults;
}
}
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
{
var configuration = ConfigurationObject as BlockGridConfiguration;
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
}
}
#endregion

View File

@@ -36,6 +36,8 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
{
}
public override bool SupportsConfigurableElements => true;
#region Pre Value Editor
protected override IConfigurationEditor CreateConfigurationEditor() =>

View File

@@ -93,6 +93,12 @@ public abstract class BlockListPropertyEditorBase : DataEditor
return ValidateNumberOfBlocks(blockEditorData, validationLimit.Min, validationLimit.Max);
}
}
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
{
var configuration = ConfigurationObject as BlockListConfiguration;
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
}
}
#endregion

View File

@@ -114,6 +114,15 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
MapBlockItemDataToEditor(property, blockValue.SettingsData);
}
protected IEnumerable<Guid> ConfiguredElementTypeKeys(IBlockConfiguration configuration)
{
yield return configuration.ContentElementTypeKey;
if (configuration.SettingsElementTypeKey is not null)
{
yield return configuration.SettingsElementTypeKey.Value;
}
}
private void MapBlockItemDataToEditor(IProperty property, List<BlockItemData> items)
{
var valEditors = new Dictionary<Guid, IDataValueEditor>();

View File

@@ -47,6 +47,8 @@ public class RichTextPropertyEditor : DataEditor
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _richTextPropertyIndexValueFactory;
public override bool SupportsConfigurableElements => true;
/// <summary>
/// Create a custom value editor
/// </summary>
@@ -238,6 +240,12 @@ public class RichTextPropertyEditor : DataEditor
return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(cleanedUpRichTextEditorValue, _jsonSerializer);
}
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
{
var configuration = ConfigurationObject as RichTextConfiguration;
return configuration?.Blocks?.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
}
private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue)
=> RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue);

View File

@@ -7,8 +7,8 @@
"name": "acceptancetest",
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.13",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.70",
"@umbraco/json-models-builders": "^2.0.14",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.73",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"faker": "^4.1.0",
@@ -132,19 +132,19 @@
}
},
"node_modules/@umbraco/json-models-builders": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.13.tgz",
"integrity": "sha512-HeI6I2BO8/3rJyinJTFxhpBSr/TaCc+S1Si+9SXIlze+Erq+yraor706mQDsgIuLfUzAYgmLLoQFxMVof/P7Kw==",
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@umbraco/json-models-builders/-/json-models-builders-2.0.14.tgz",
"integrity": "sha512-fP6hVSSph1iFQ1c65UH80AM6QK3r1CzuIiYOvZh+QOoVzpVFtH1VCHL3J2k8AwaHWLVAEopcvtvH5kkl7Luqww==",
"dependencies": {
"camelize": "^1.0.1"
}
},
"node_modules/@umbraco/playwright-testhelpers": {
"version": "2.0.0-beta.70",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.70.tgz",
"integrity": "sha512-voNem+L8Nct5bsjbS8HLWK2BCOh7gu834OS487Os1Mj4ebZbHk04YkBYNh7X8HWLNZ1NYqTquAWbFb9RxEgnnw==",
"version": "2.0.0-beta.73",
"resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-2.0.0-beta.73.tgz",
"integrity": "sha512-CCURatZa7Ipui9ZTqdZmkpx89Sr5AJLoXogniq6mv84mSVGeCQFYzHvw1op2UE8nkKY5/wyqfrCihjrbW5v8lw==",
"dependencies": {
"@umbraco/json-models-builders": "2.0.13",
"@umbraco/json-models-builders": "2.0.14",
"node-fetch": "^2.6.7"
}
},

View File

@@ -21,8 +21,8 @@
"wait-on": "^7.2.0"
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.13",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.70",
"@umbraco/json-models-builders": "^2.0.14",
"@umbraco/playwright-testhelpers": "^2.0.0-beta.73",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",
"faker": "^4.1.0",

View File

@@ -24,7 +24,7 @@ test.afterEach(async ({umbracoApi}) => {
test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
@@ -33,11 +33,11 @@ test('can create child node', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) =
await umbracoUi.content.clickActionsMenuForContent(contentName);
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(childDocumentTypeName);
// This wait is needed
// This wait is needed
await umbracoUi.waitForTimeout(500);
await umbracoUi.content.enterContentName(childContentName);
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(childContentName)).toBeTruthy();
@@ -61,9 +61,9 @@ test('can create child node in child node', async ({umbracoApi, umbracoUi}) => {
let childContentId: any;
await umbracoApi.documentType.ensureNameNotExists(childOfChildDocumentTypeName);
childOfChildDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childOfChildDocumentTypeName);
childDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(childDocumentTypeName, childOfChildDocumentTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
childDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(childDocumentTypeName, childOfChildDocumentTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
childContentId = await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
@@ -73,7 +73,7 @@ test('can create child node in child node', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.content.clickActionsMenuForContent(childContentName);
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(childOfChildDocumentTypeName);
// This wait is needed
// This wait is needed
await umbracoUi.waitForTimeout(500);
await umbracoUi.content.enterContentName(childOfChildContentName);
await umbracoUi.content.clickSaveButton();
@@ -94,7 +94,7 @@ test('can create child node in child node', async ({umbracoApi, umbracoUi}) => {
test('cannot publish child if the parent is not published', async ({umbracoApi, umbracoUi}) => {
// Arrange
childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId);
await umbracoUi.goToBackOffice();
@@ -114,7 +114,7 @@ test('cannot publish child if the parent is not published', async ({umbracoApi,
test('can publish with descendants', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
childDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childDocumentTypeName);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(documentTypeName, childDocumentTypeId);
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(childContentName, childDocumentTypeId, contentId);
await umbracoUi.goToBackOffice();

View File

@@ -91,7 +91,7 @@ test('can rename content', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(wrongContentName);
await umbracoUi.content.goToContentWithName(wrongContentName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButton();
@@ -111,7 +111,7 @@ test('can update content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterTextstring(contentText);
await umbracoUi.content.clickSaveButton();
@@ -159,4 +159,4 @@ test('can unpublish content', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) =
await umbracoUi.content.isSuccessNotificationVisible();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe('Draft');
});
});

View File

@@ -27,11 +27,11 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickInfoTab();
await umbracoUi.content.doesLinkHaveText(notPublishContentLink);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
const contentData = await umbracoApi.document.get(contentId);
// TODO: Uncomment this when front-end is ready. Currently the link is not updated immediately after publishing
@@ -58,7 +58,7 @@ test('can open document type', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickDocumentTypeByName(documentTypeName);
// Assert
@@ -74,9 +74,9 @@ test('can open template', async ({umbracoApi, umbracoUi}) => {
contentId = await umbracoApi.document.createDocumentWithTemplate(contentName, documentTypeId, templateId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickTemplateByName(templateName);
// Assert
@@ -100,7 +100,7 @@ test('can change template', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.changeTemplate(firstTemplateName, secondTemplateName);
await umbracoUi.content.clickSaveButton();
@@ -127,7 +127,7 @@ test('cannot change to a template that is not allowed in the document type', asy
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickEditTemplateByName(firstTemplateName);
// Assert

View File

@@ -98,7 +98,7 @@ test('can choose start node for the content picker in the content', async ({umbr
await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName);
const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName);
const contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId);
// Create a custom content picker with start node
const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithStartNode(customDataTypeName, contentPickerId);
@@ -129,7 +129,7 @@ test.skip('can ignore user start node for the content picker in the content', as
await umbracoApi.documentType.ensureNameNotExists(childContentPickerDocumentTypeName);
const childContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentType(childContentPickerDocumentTypeName);
const contentPickerDocumentTypeId = await umbracoApi.documentType.createDocumentTypeWithAllowedChildNode(contentPickerName, childContentPickerDocumentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
const contentPickerId = await umbracoApi.document.createDefaultDocument(contentPickerName, contentPickerDocumentTypeId);
await umbracoApi.document.createDefaultDocumentWithParent(childContentPickerName, childContentPickerDocumentTypeId, contentPickerId);
// Create a custom content picker with the setting "ignore user start node" is enable
const customDataTypeId = await umbracoApi.dataType.createContentPickerDataTypeWithIgnoreUserStartNodes(customDataTypeName, contentPickerId);
@@ -161,7 +161,7 @@ test('can remove content picker in the content', async ({umbracoApi, umbracoUi})
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.removeContentPicker(contentPickerName);
await umbracoUi.content.clickSaveButton();

View File

@@ -0,0 +1,257 @@
import {AliasHelper, ConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const dataTypeName = 'Image Media Picker';
const customDataTypeName = 'Custom Image Media Picker';
const groupName = 'TestGroup';
const mediaName = 'TestImage';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('can save content with a image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataType = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
});
test('can publish content with a image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataType = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
});
test('can add an image to the image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataType = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.media.ensureNameNotExists(mediaName);
const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.selectMediaByName(mediaName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesImageMediaPickerContainImage(contentName, AliasHelper.toAlias(dataTypeName), imageId)).toBeTruthy();
// Clean
await umbracoApi.media.ensureNameNotExists(mediaName);
});
test('can remove an image from the image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataType = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.media.ensureNameNotExists(mediaName);
const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataType.id, groupName);
await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(dataTypeName), imageId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickRemoveButtonForName(mediaName);
await umbracoUi.content.clickConfirmRemoveButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesImageMediaPickerContainImage(contentName, AliasHelper.toAlias(dataTypeName), imageId)).toBeFalsy();
// Clean
await umbracoApi.media.ensureNameNotExists(mediaName);
});
// TODO: Remove skip when the front-end is ready as there are currently no displayed error notification.
test.skip('image count can not be less that min amount set in image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 1);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isErrorNotificationVisible();
// Clean
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
// TODO: Remove skip when the front-end is ready as there are currently no displayed error notification.
test.skip('image count can not be more that max amount set in image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 0, 0);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.selectMediaByName(mediaName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isErrorNotificationVisible();
// Clean
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can add an image from the image media picker with a start node', async ({umbracoApi, umbracoUi}) => {
const mediaFolderName = 'TestFolder';
await umbracoApi.media.ensureNameNotExists(mediaFolderName);
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
const imageFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName);
const imageId = await umbracoApi.media.createDefaultMediaWithImageAndParentId(mediaName, imageFolderId);
const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataTypeWithStartNodeId(customDataTypeName, imageFolderId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.selectMediaByName(mediaName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesImageMediaPickerContainImage(contentName, AliasHelper.toAlias(customDataTypeName), imageId)).toBeTruthy();
// Clean
await umbracoApi.media.ensureNameNotExists(mediaFolderName);
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can add an image from the image media picker with focal point enabled', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 0, 1, true);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName);
await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(customDataTypeName), imageId);
const widthPercentage = 40;
const heightPercentage = 20;
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickExactLinkWithName(mediaName);
await umbracoUi.content.setFocalPoint(widthPercentage, heightPercentage);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesImageMediaPickerContainImageWithFocalPoint(contentName, AliasHelper.toAlias(customDataTypeName), imageId, {left: 0.4, top: 0.2})).toBeTruthy();
// Clean
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can reset focal point in a image from the image media picker', async ({umbracoApi, umbracoUi}) => {
// Arrange
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataType(customDataTypeName, 0, 1, true);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName);
await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(customDataTypeName), imageId, {left: 0.4, top: 0.2});
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickExactLinkWithName(mediaName);
await umbracoUi.content.clickResetFocalPointButton();
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
expect(await umbracoApi.document.doesImageMediaPickerContainImageWithFocalPoint(contentName, AliasHelper.toAlias(customDataTypeName), imageId, {left: 0, top: 0})).toBeTruthy();
// Clean
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
// TODO: Remove skip when the front-end is ready as currently the crop is not being selected.
test.skip('can add an image from the image media picker with a image crop', async ({umbracoApi, umbracoUi}) => {
// Arrange
const cropLabel = 'TestCrop';
const cropWidth = 100;
const cropHeight = 100;
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
const imageId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
const dataTypeId = await umbracoApi.dataType.createImageMediaPickerDataTypeWithCrop(customDataTypeName, cropLabel, cropWidth, cropHeight);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, dataTypeId, groupName);
await umbracoApi.document.createDocumentWithImageMediaPicker(contentName, documentTypeId, AliasHelper.toAlias(customDataTypeName), imageId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
});

View File

@@ -12,13 +12,13 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.media.ensureNameNotExists(mediaFileName);
mediaFileId = await umbracoApi.media.createDefaultMedia(mediaFileName, mediaTypeName);
mediaFileId = await umbracoApi.media.createDefaultMediaFile(mediaFileName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.media.ensureNameNotExists(mediaFileName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
@@ -33,7 +33,6 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickChooseMediaPickerButton();
await umbracoUi.content.selectMediaByName(mediaFileName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
@@ -60,7 +59,6 @@ test('can publish content with the media picker data type', async ({umbracoApi,
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickChooseMediaPickerButton();
await umbracoUi.content.selectMediaByName(mediaFileName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveAndPublishButton();
@@ -84,7 +82,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi})
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.removeMediaPickerByName(mediaFileName);
await umbracoUi.content.clickSaveButton();
@@ -103,7 +101,7 @@ test('can limit the media picker in the content by setting the start node', asyn
await umbracoApi.media.ensureNameNotExists(mediaFolderName);
const mediaFolderId = await umbracoApi.media.createDefaultMediaFolder(mediaFolderName);
await umbracoApi.media.ensureNameNotExists(childMediaName);
await umbracoApi.media.createDefaultMedia(childMediaName, mediaTypeName, mediaFolderId);
await umbracoApi.media.createDefaultMediaFileAndParentId(childMediaName, mediaFolderId);
const customDataTypeId = await umbracoApi.dataType.createMediaPickerDataTypeWithStartNodeId(customDataTypeName, mediaFolderId);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);

View File

@@ -15,16 +15,16 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.media.ensureNameNotExists(firstMediaFileName);
firstMediaFileId = await umbracoApi.media.createDefaultMedia(firstMediaFileName, firstMediaTypeName);
firstMediaFileId = await umbracoApi.media.createDefaultMediaFile(firstMediaFileName);
await umbracoApi.media.ensureNameNotExists(secondMediaFileName);
secondMediaFileId = await umbracoApi.media.createDefaultMedia(secondMediaFileName, secondMediaTypeName);
secondMediaFileId = await umbracoApi.media.createDefaultMediaWithImage(secondMediaFileName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.media.ensureNameNotExists(firstMediaFileName);
await umbracoApi.media.ensureNameNotExists(secondMediaFileName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
@@ -39,8 +39,8 @@ test('can create content with multiple media picker data type', async ({umbracoA
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickChooseMediaPickerButton();
await umbracoUi.content.selectMediaByName(firstMediaFileName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.selectMediaByName(secondMediaFileName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveButton();
@@ -68,8 +68,8 @@ test('can publish content with multiple media picker data type', async ({umbraco
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickChooseMediaPickerButton();
await umbracoUi.content.selectMediaByName(firstMediaFileName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.selectMediaByName(secondMediaFileName);
await umbracoUi.content.clickSubmitButton();
await umbracoUi.content.clickSaveAndPublishButton();
@@ -94,7 +94,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi})
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.removeMediaPickerByName(firstMediaFileName);
await umbracoUi.content.clickSaveButton();

View File

@@ -13,7 +13,7 @@ test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.redirectManagement.setStatus(enableStatus);
await umbracoUi.goToBackOffice();
// Create a content
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
documentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(documentTypeName);
contentId = await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
// Publish the content
@@ -36,10 +36,10 @@ test('can disable URL tracker', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.redirectManagement.clickDisableButton();
// Assert
// Verfiy that if renaming a published page, there are no redirects have been made
// rename the published content
// Verify that if renaming a published page, there are no redirects have been made
// rename the published content
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterContentName(updatedContentName);
await umbracoUi.content.clickSaveAndPublishButton();
// verify that there is no redirects have been made
@@ -63,10 +63,10 @@ test.skip('can re-enable URL tracker', async ({umbracoApi, umbracoUi}) => {
await umbracoUi.redirectManagement.clickEnableURLTrackerButton();
// Assert
// Verfiy that if renaming a published page, there are one redirects have been made
// rename the published content
// Verify that if renaming a published page, there are one redirects have been made
// rename the published content
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterContentName(updatedContentName);
await umbracoUi.content.clickSaveAndPublishButton();
// verify that there is one redirects have been made
@@ -90,7 +90,7 @@ test.skip('can search for original URL', async ({umbracoUi}) => {
await umbracoUi.redirectManagement.clickRedirectManagementTab();
await umbracoUi.redirectManagement.enterOriginalUrl(searchKeyword);
await umbracoUi.redirectManagement.clickSearchButton();
// Assert
// TODO: verify the search result
});
@@ -98,9 +98,9 @@ test.skip('can search for original URL', async ({umbracoUi}) => {
// TODO: Remove skip when the frond-end is ready. Currently there is no redirect have been made after renaming a published page
test.skip('can delete a redirect', async ({umbracoApi, umbracoUi}) => {
// Arrange
// Rename the published content
// Rename the published content
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.openContent(contentName);
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.enterContentName(updatedContentName);
await umbracoUi.content.clickSaveAndPublishButton();

View File

@@ -224,12 +224,11 @@ test('can remove a content start node from a user', {tag: '@smoke'}, async ({umb
test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => {
// Arrange
const mediaTypeName = 'File';
const mediaName = 'TestMediaFile';
const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName);
await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]);
await umbracoApi.media.ensureNameNotExists(mediaName);
const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName);
const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName);
await umbracoUi.user.goToSection(ConstantHelper.sections.users);
// Act
@@ -249,15 +248,14 @@ test('can add media start nodes for a user', {tag: '@smoke'}, async ({umbracoApi
test('can add multiple media start nodes for a user', async ({umbracoApi, umbracoUi}) => {
// Arrange
const mediaTypeName = 'File';
const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName);
const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]);
const mediaName = 'TestMediaFile';
const secondMediaName = 'SecondMediaFile';
await umbracoApi.media.ensureNameNotExists(mediaName);
await umbracoApi.media.ensureNameNotExists(secondMediaName);
const firstMediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName);
const secondMediaId = await umbracoApi.media.createDefaultMedia(secondMediaName, mediaTypeName);
const firstMediaId = await umbracoApi.media.createDefaultMediaFile(mediaName);
const secondMediaId = await umbracoApi.media.createDefaultMediaFile(secondMediaName);
// Adds the media start node to the user
const userData = await umbracoApi.user.getByName(nameOfTheUser);
userData.mediaStartNodeIds.push({id: firstMediaId});
@@ -283,12 +281,11 @@ test('can add multiple media start nodes for a user', async ({umbracoApi, umbrac
test('can remove a media start node from a user', async ({umbracoApi, umbracoUi}) => {
// Arrange
const mediaTypeName = 'File';
const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName);
const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]);
const mediaName = 'TestMediaFile';
await umbracoApi.media.ensureNameNotExists(mediaName);
const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName);
const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName);
// Adds the media start node to the user
const userData = await umbracoApi.user.getByName(nameOfTheUser);
userData.mediaStartNodeIds.push({id: mediaId});
@@ -374,12 +371,11 @@ test('can see if the user has the correct access based on content start nodes',
test('can see if the user has the correct access based on media start nodes', async ({umbracoApi, umbracoUi}) => {
// Arrange
const mediaTypeName = 'File';
const userGroup = await umbracoApi.userGroup.getByName(defaultUserGroupName);
const userId = await umbracoApi.user.createDefaultUser(nameOfTheUser, userEmail, [userGroup.id]);
const mediaName = 'TestMediaFile';
await umbracoApi.media.ensureNameNotExists(mediaName);
const mediaId = await umbracoApi.media.createDefaultMedia(mediaName, mediaTypeName);
const mediaId = await umbracoApi.media.createDefaultMediaFile(mediaName);
// Adds the media start node to the user
const userData = await umbracoApi.user.getByName(nameOfTheUser);
userData.mediaStartNodeIds.push({id: mediaId});

View File

@@ -0,0 +1,358 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
using Umbraco.Cms.Tests.Common.Attributes;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class ElementSwitchValidatorTests : UmbracoIntegrationTest
{
private IElementSwitchValidator ElementSwitchValidator => GetRequiredService<IElementSwitchValidator>();
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
private IContentService ContentService => GetRequiredService<IContentService>();
private IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();
[TestCase(new[] { true }, 0, true, true, TestName = "E=>E No Ancestor or children")]
[TestCase(new[] { false }, 0, false, true, TestName = "D=>D No Ancestor or children")]
[TestCase(new[] { true }, 0, false, true, TestName = "E=>D No Ancestor or children")]
[TestCase(new[] { false }, 0, true, true, TestName = "D=>E No Ancestor or children")]
[TestCase(new[] { true, true }, 1, true, true, TestName = "E Valid Parent")]
[TestCase(new[] { true, true }, 0, true, true, TestName = "E Valid Child")]
[TestCase(new[] { false, false }, 1, false, true, TestName = "D Valid Parent")]
[TestCase(new[] { false, false }, 0, false, true, TestName = "D Valid Child")]
[TestCase(new[] { false, false }, 1, true, false, TestName = "E InValid Parent")]
[TestCase(new[] { false, false }, 0, true, true, TestName = "E InValid Child")]
[TestCase(new[] { true, true }, 1, false, false, TestName = "D InValid Parent")]
[TestCase(new[] { true, true }, 0, false, true, TestName = "D InValid Child")]
[TestCase(new[] { true, false, false, true, false }, 2, true, false,
TestName = "D=>E InValid Child, Invalid Parent")]
[TestCase(new[] { false, true, false, true, false }, 2, true, false,
TestName = "D=>E InValid Child, Invalid Ancestor")]
[TestCase(new[] { true, false, false, true, true }, 2, true, false,
TestName = "D=>E Valid Children, Invalid Parent")]
[TestCase(new[] { false, true, false, true, true }, 2, true, false,
TestName = "D=>E Valid Children, Invalid Ancestor")]
[TestCase(new[] { false, false, false, false, false }, 2, true, false, TestName = "D=>E mismatch")]
[TestCase(new[] { false, false, true, false, false }, 2, false, true, TestName = "D=>E correction")]
[TestCase(new[] { true, true, true, true, true }, 2, false, false, TestName = "E=>D mismatch")]
[TestCase(new[] { true, true, false, true, true }, 2, true, true, TestName = "E=>D correction")]
[LongRunning]
public async Task AncestorsAreAligned(
bool[] isElementDoctypeChain,
int itemToTestIndex,
bool itemToTestNewIsElementValue,
bool validationShouldPass)
{
// Arrange
IContentType? parentItem = null;
IContentType? itemToTest = null;
for (var index = 0; index < isElementDoctypeChain.Length; index++)
{
var itemIsElement = isElementDoctypeChain[index];
var builder = new ContentTypeBuilder()
.WithIsElement(itemIsElement);
if (parentItem is not null)
{
builder.WithParentContentType(parentItem);
}
var contentType = builder.Build();
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
parentItem = contentType;
if (index == itemToTestIndex)
{
itemToTest = contentType;
}
}
// Act
itemToTest!.IsElement = itemToTestNewIsElementValue;
var result = await ElementSwitchValidator.AncestorsAreAlignedAsync(itemToTest);
// Assert
Assert.AreEqual(result, validationShouldPass);
}
[TestCase(new[] { true }, 0, true, true, TestName = "E=>E No Ancestor or children")]
[TestCase(new[] { false }, 0, false, true, TestName = "D=>D No Ancestor or children")]
[TestCase(new[] { true }, 0, false, true, TestName = "E=>D No Ancestor or children")]
[TestCase(new[] { false }, 0, true, true, TestName = "D=>E No Ancestor or children")]
[TestCase(new[] { true, true }, 1, true, true, TestName = "E Valid Parent")]
[TestCase(new[] { true, true }, 0, true, true, TestName = "E Valid Child")]
[TestCase(new[] { false, false }, 1, false, true, TestName = "D Valid Parent")]
[TestCase(new[] { false, false }, 0, false, true, TestName = "D Valid Child")]
[TestCase(new[] { false, false }, 1, true, true, TestName = "E InValid Parent")]
[TestCase(new[] { false, false }, 0, true, false, TestName = "E InValid Child")]
[TestCase(new[] { true, true }, 1, false, true, TestName = "D InValid Parent")]
[TestCase(new[] { true, true }, 0, false, false, TestName = "D InValid Child")]
[TestCase(new[] { true, false, false, true, false }, 2, true, false,
TestName = "D=>E InValid Child, Invalid Parent")]
[TestCase(new[] { false, true, false, true, false }, 2, true, false,
TestName = "D=>E InValid Child, Invalid Ancestor")]
[TestCase(new[] { true, false, false, true, true }, 2, true, true,
TestName = "D=>E Valid Children, Invalid Parent")]
[TestCase(new[] { false, true, false, true, true }, 2, true, true,
TestName = "D=>E Valid Children, Invalid Ancestor")]
[TestCase(new[] { false, false, false, false, false }, 2, true, false, TestName = "D=>E mismatch")]
[TestCase(new[] { false, false, true, false, false }, 2, false, true, TestName = "D=>E correction")]
[TestCase(new[] { true, true, true, true, true }, 2, false, false, TestName = "E=>D mismatch")]
[TestCase(new[] { true, true, false, true, true }, 2, true, true, TestName = "E=>D correction")]
[LongRunning]
public async Task DescendantsAreAligned(
bool[] isElementDoctypeChain,
int itemToTestIndex,
bool itemToTestNewIsElementValue,
bool validationShouldPass)
{
// Arrange
IContentType? parentItem = null;
IContentType? itemToTest = null;
for (var index = 0; index < isElementDoctypeChain.Length; index++)
{
var itemIsElement = isElementDoctypeChain[index];
var builder = new ContentTypeBuilder()
.WithIsElement(itemIsElement);
if (parentItem is not null)
{
builder.WithParentContentType(parentItem);
}
var contentType = builder.Build();
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
parentItem = contentType;
if (index == itemToTestIndex)
{
itemToTest = contentType;
}
}
// Act
itemToTest!.IsElement = itemToTestNewIsElementValue;
var result = await ElementSwitchValidator.DescendantsAreAlignedAsync(itemToTest);
// Assert
Assert.AreEqual(result, validationShouldPass);
}
[TestCase(0, true, TestName = "No Content")]
[TestCase(1, false, TestName = "One Content Item")]
[TestCase(5, false, TestName = "Many Content Items")]
public async Task DocumentToElementHasNoContent(int amountOfDocumentsCreated, bool validationShouldPass)
{
// Arrange
var contentType = await SetupContentType(false);
for (int i = 0; i < amountOfDocumentsCreated; i++)
{
var contentBuilder = new ContentBuilder().WithContentType(contentType);
var content = contentBuilder.Build();
ContentService.Save(content);
}
// Act
contentType.IsElement = true;
var result = await ElementSwitchValidator.DocumentToElementHasNoContentAsync(contentType);
// Assert
Assert.AreEqual(result, validationShouldPass);
}
// Since the full permutation table would result in 64 tests and more block editors might be added later,
// we will at least test each single failure and a few combinations
// used in none
[TestCase(false, false, false, false, false, false, true)]
// used in one
[TestCase(true, false, false, false, false, false, false)]
[TestCase(false, true, false, false, false, false, false)]
[TestCase(false, false, true, false, false, false, false)]
[TestCase(false, false, false, true, false, false, false)]
[TestCase(false, false, false, false, true, false, false)]
[TestCase(false, false, false, false, false, true, false)]
// used in selection and setting
[TestCase(true, true, false, false, false, false, false)]
// used in 2 selections
[TestCase(true, false, true, false, false, false, false)]
// used in 2 settings
[TestCase(false, true, false, false, false, true, false)]
// used in all
[TestCase(true, true, true, true, true, true, false)]
public async Task ElementToDocumentNotUsedInBlockStructures(
bool isUsedInBlockList,
bool isUsedInBlockListBlockSetting,
bool isUsedInBlockGrid,
bool isUsedInBlockGridBlockSetting,
bool isUsedInRte,
bool isUsedInRteBlockSetting,
bool validationShouldPass)
{
// Arrange
var elementType = await SetupContentType(true);
var otherElementType = await SetupContentType(true);
if (isUsedInBlockList)
{
await SetupDataType(Constants.PropertyEditors.Aliases.BlockList, elementType.Key, null);
}
if (isUsedInBlockListBlockSetting)
{
await SetupDataType(Constants.PropertyEditors.Aliases.BlockList, otherElementType.Key, elementType.Key);
}
if (isUsedInBlockGrid)
{
await SetupDataType(Constants.PropertyEditors.Aliases.BlockGrid, elementType.Key, null);
}
if (isUsedInBlockGridBlockSetting)
{
await SetupDataType(Constants.PropertyEditors.Aliases.BlockGrid, otherElementType.Key, elementType.Key);
}
if (isUsedInRte)
{
await SetupDataType(Constants.PropertyEditors.Aliases.RichText, elementType.Key, null);
}
if (isUsedInRteBlockSetting)
{
await SetupDataType(Constants.PropertyEditors.Aliases.RichText, otherElementType.Key, elementType.Key);
}
// Act
var result = await ElementSwitchValidator.ElementToDocumentNotUsedInBlockStructuresAsync(elementType);
// Assert
Assert.AreEqual(result, validationShouldPass);
}
private async Task<IContentType> SetupContentType(bool isElement)
{
var typeBuilder = new ContentTypeBuilder()
.WithIsElement(isElement);
var contentType = typeBuilder.Build();
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
return contentType;
}
// Constants.PropertyEditors.Aliases.BlockGrid
private async Task SetupDataType(
string editorAlias,
Guid elementKey,
Guid? elementSettingKey)
{
Dictionary<string, object> configuration;
switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockGrid:
configuration = GetBlockGridBaseConfiguration();
break;
case Constants.PropertyEditors.Aliases.RichText:
configuration = GetRteBaseConfiguration();
break;
default:
configuration = new Dictionary<string, object>();
break;
}
SetBlockConfiguration(
configuration,
elementKey,
elementSettingKey,
editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null);
var dataTypeBuilder = new DataTypeBuilder()
.WithId(0)
.WithDatabaseType(ValueStorageType.Nvarchar)
.AddEditor()
.WithAlias(editorAlias);
switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockGrid:
dataTypeBuilder.WithConfigurationEditor(
new BlockGridConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
break;
case Constants.PropertyEditors.Aliases.BlockList:
dataTypeBuilder.WithConfigurationEditor(
new BlockListConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
break;
case Constants.PropertyEditors.Aliases.RichText:
dataTypeBuilder.WithConfigurationEditor(
new RichTextConfigurationEditor(IOHelper) { DefaultConfiguration = configuration });
break;
}
var dataType = dataTypeBuilder.Done()
.Build();
await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey);
}
private void SetBlockConfiguration(
Dictionary<string, object> dictionary,
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
if (elementKey is null)
{
return;
}
dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
}
private Dictionary<string, object> GetBlockGridBaseConfiguration()
=> new Dictionary<string, object> { ["gridColumns"] = 12 };
private Dictionary<string, object> GetRteBaseConfiguration()
{
var dictionary = new Dictionary<string, object>
{
["maxImageSize"] = 500,
["mode"] = "Classic",
["toolbar"] = new[]
{
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist",
"outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
},
};
return dictionary;
}
private Dictionary<string, object> BuildBlockConfiguration(
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
var dictionary = new Dictionary<string, object>();
if (allowAtRoot is not null)
{
dictionary.Add("allowAtRoot", allowAtRoot.Value);
}
dictionary.Add("contentElementTypeKey", elementKey.ToString());
if (elementSettingKey is not null)
{
dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString());
}
return dictionary;
}
}

View File

@@ -332,6 +332,22 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest
Assert.AreEqual(DomainOperationStatus.DuplicateDomainName, result.Status);
}
[TestCase("https://*.umbraco.com")]
[TestCase("&#€%#€")]
[TestCase("¢”$¢”¢$≈{")]
public async Task Cannot_Assign_Invalid_Domains(string domainName)
{
var domainService = GetRequiredService<IDomainService>();
var updateModel = new DomainsUpdateModel
{
Domains = new DomainModel { DomainName = domainName, IsoCode = Cultures.First() }.Yield()
};
var result = await domainService.UpdateDomainsAsync(Root.Key, updateModel);
Assert.IsFalse(result.Success);
Assert.AreEqual(DomainOperationStatus.InvalidDomainName, result.Status);
}
[Test]
public async Task Cannot_Assign_Already_Used_Domains()
{

View File

@@ -11,6 +11,7 @@ using NUnit.Framework;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Tests.Common.TestHelpers;
using Umbraco.Extensions;
using DateTimeOffset = System.DateTimeOffset;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.CoreThings;
@@ -332,6 +333,50 @@ public class ObjectExtensionsTests
Assert.AreEqual("This is a string", conv.Result);
}
[Test]
public void CanConvertDateTimeOffsetToDateTime()
{
var dateTimeOffset = new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero);
var result = dateTimeOffset.TryConvertTo<DateTime>();
Assert.IsTrue(result.Success);
Assert.Multiple(() =>
{
Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03)), result.Result);
Assert.AreEqual(DateTimeKind.Utc, result.Result.Kind);
});
}
[Test]
public void CanConvertDateTimeToDateTimeOffset()
{
var dateTime = new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), DateTimeKind.Utc);
var result = dateTime.TryConvertTo<DateTimeOffset>();
Assert.IsTrue(result.Success);
Assert.AreEqual(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero), result.Result);
}
[Test]
public void DiscardsOffsetWhenConvertingDateTimeOffsetToDateTime()
{
var dateTimeOffset = new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.FromHours(2));
var result = dateTimeOffset.TryConvertTo<DateTime>();
Assert.IsTrue(result.Success);
Assert.Multiple(() =>
{
Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03)), result.Result);
Assert.AreEqual(DateTimeKind.Utc, result.Result.Kind);
});
}
[Test]
public void DiscardsDateTimeKindWhenConvertingDateTimeToDateTimeOffset()
{
var dateTime = new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), DateTimeKind.Local);
var result = dateTime.TryConvertTo<DateTimeOffset>();
Assert.IsTrue(result.Success);
Assert.AreEqual(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero), result.Result);
}
[Test]
public void Value_Editor_Can_Convert_Decimal_To_Decimal_Clr_Type()
{
@@ -342,6 +387,23 @@ public class ObjectExtensionsTests
Assert.AreEqual(12.34d, result.Result);
}
[Test]
public void Value_Editor_Can_Convert_DateTimeOffset_To_DateTime_Clr_Type()
{
var valueEditor = MockedValueEditors.CreateDataValueEditor(ValueTypes.Date);
var result = valueEditor.TryConvertValueToCrlType(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30), TimeSpan.Zero));
Assert.IsTrue(result.Success);
Assert.IsTrue(result.Result is DateTime);
var dateTime = (DateTime)result.Result;
Assert.Multiple(() =>
{
Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30)), dateTime);
Assert.AreEqual(DateTimeKind.Utc, dateTime.Kind);
});
}
private class MyTestObject
{
public override string ToString() => "Hello world";

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
"version": "14.2.0-rc",
"version": "14.3.0-rc",
"assemblyVersion": {
"precision": "build"
},