Merge branch 'v14/dev' into contrib
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
|
||||
@@ -45275,4 +45275,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,4 +21,7 @@ public enum ContentTypeOperationStatus
|
||||
NotFound,
|
||||
NotAllowed,
|
||||
CancelledByNotification,
|
||||
InvalidElementFlagDocumentHasContent,
|
||||
InvalidElementFlagElementIsUsedInPropertyEditorConfiguration,
|
||||
InvalidElementFlagComparedToParent,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ public enum DomainOperationStatus
|
||||
ContentNotFound,
|
||||
LanguageNotFound,
|
||||
DuplicateDomainName,
|
||||
ConflictingDomainName
|
||||
ConflictingDomainName,
|
||||
InvalidDomainName
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public class BlockGridPropertyEditor : BlockGridPropertyEditorBase
|
||||
: base(dataValueEditorFactory, blockValuePropertyIndexValueFactory)
|
||||
=> _ioHelper = ioHelper;
|
||||
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,6 +36,8 @@ public class BlockListPropertyEditor : BlockListPropertyEditorBase
|
||||
{
|
||||
}
|
||||
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Submodule src/Umbraco.Web.UI.Client updated: 97f256a195...bb6abdc884
18
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
18
tests/Umbraco.Tests.AcceptanceTest/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user