Compare commits

...
Author SHA1 Message Date
Andy ButlandandNiels Lyngsø 8a6615da76 Make sure not to early return when verifying ancestor path is published without completing scope (#19029)
* Make sure not to early return when verifying ancestor path is published without completing scope.

* Added comment.
2025-04-14 20:31:46 +02:00
a5d1b7c122 Use maximum available value for JSON serialization depth (#19001)
* Use maximum available value for JSON serialization depth.

* Updated unittests with best practices

---------

Co-authored-by: Migaroez <geusens@gmail.com>
2025-04-14 20:31:46 +02:00
Nhu DinhandNiels Lyngsø 3001e6dbff V15 Updated acceptance tests for unsupported invariant content item with variant blocks (#18919)
* Updated tests for invariant block grid with variant blocks

* Updated tests for invariant block list with variant blocks

* Updated tests for invariant document with variant blocks

* Bumped version

* Changed npm command

* Updated isValidationMessageVisible

* Fixed name

* Fixed comments

* Reverted npm command
2025-04-14 20:31:46 +02:00
Andy ButlandandNiels Lyngsø b917b61dac Ensure dates read from the database are treated as local when constructing entities (#18989)
* Ensure dates read from the database are treated as local when constructing entities.

* Fixed typos in comments.
2025-04-14 20:31:45 +02:00
Nhu DinhandNiels Lyngsø 74a517a2e1 V15 Added acceptance tests for the mandatory property editor in a content or a block (#18920)
* Added tests for mandatory checkboxlist in a content

* Added tests for mandatory dropdown in a content

* Added tests for mandatory media picker in a content

* Added tests for mandatory radiobox in a content

* Added tests for block grid with mandatory property editors

* Added tests for block list with mandatory property editors

* Bumped version

* Cleaned up

* Changed npm command

* Fixed comments

* Fixed comments

* Reverted
2025-04-14 20:31:45 +02:00
dependabot[bot]andNiels Lyngsø 706a0983df Bump vite from 6.2.5 to 6.2.6 in /src/Umbraco.Web.UI.Client
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 20:31:22 +02:00
dependabot[bot]andNiels Lyngsø bb419ec2c6 Bump vite from 6.2.5 to 6.2.6 in /src/Umbraco.Web.UI.Login
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.5 to 6.2.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.2.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.2.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.2.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 20:31:22 +02:00
kjacandNiels Lyngsø 018d49dda2 Only return the direct compositions of a content type - not its ancestor compositions. Effectively a revert of #15800 2025-04-14 20:31:22 +02:00
kjacandNiels Lyngsø 2fbdc291c9 Do not reject existing compositions when updating content types 2025-04-14 20:31:22 +02:00
Niels Lyngsø 9148a0f845 without UMB_ 2025-04-14 20:31:22 +02:00
leekelleherandNiels Lyngsø 2f7383a622 Implements "inheritance" composition 2025-04-14 20:31:22 +02:00
kjacandNiels Lyngsø b306c16d10 Accept parent key equal to inheritance composition key when creating document and media types 2025-04-14 20:30:05 +02:00
leekelleherandNiels Lyngsø 9288854d6b [WIP] Temp fix for circular reference 2025-04-14 20:30:04 +02:00
leekelleherandNiels Lyngsø 1565665a40 Nested document types (client) 2025-04-14 20:28:55 +02:00
38 changed files with 916 additions and 277 deletions
@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
using ContentTypeEditingModels = Umbraco.Cms.Core.Models.ContentTypeEditing;
@@ -38,12 +39,27 @@ internal abstract class ContentTypeEditingPresentationFactory<TContentType>
VariesByCulture = viewModel.VariesByCulture,
VariesBySegment = viewModel.VariesBySegment,
Containers = MapContainers<TPropertyTypeContainerEditingModel>(viewModel.Containers),
Properties = MapProperties<TPropertyTypeEditingModel>(viewModel.Properties)
Properties = MapProperties<TPropertyTypeEditingModel>(viewModel.Properties),
};
return editingModel;
}
protected Guid? CalculateCreateContainerKey(ReferenceByIdModel? parent, IDictionary<Guid, ContentTypeViewModels.CompositionType> compositions)
{
// special case:
// the API is somewhat confusing when it comes to inheritance. the parent denotes a container (folder), but it
// is easily confused with the parent for inheritance.
// if the request model contains the same key for container and "inheritance composition", we'll be lenient and
// allow it - just remove the container, inheritance takes precedence as intent.
Guid? parentId = parent?.Id;
return parentId.HasValue
&& compositions.TryGetValue(parentId.Value, out ContentTypeViewModels.CompositionType compositionType)
&& compositionType is ContentTypeViewModels.CompositionType.Inheritance
? null
: parentId;
}
protected T MapCompositionModel<T>(ContentTypeAvailableCompositionsResult compositionResult)
where T : ContentTypeViewModels.AvailableContentTypeCompositionResponseModelBase, new()
{
@@ -25,12 +25,14 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
MapCleanup(createModel, requestModel.Cleanup);
createModel.Key = requestModel.Id;
createModel.ContainerKey = requestModel.Parent?.Id;
createModel.AllowedTemplateKeys = requestModel.AllowedTemplates.Select(reference => reference.Id).ToArray();
createModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id;
createModel.ListView = requestModel.Collection?.Id;
createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes);
createModel.Compositions = MapCompositions(requestModel.Compositions);
IDictionary<Guid, ViewModels.ContentType.CompositionType> compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions);
createModel.Compositions = MapCompositions(compositionTypesByKey);
createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey);
return createModel;
}
@@ -51,7 +53,7 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
updateModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id;
updateModel.ListView = requestModel.Collection?.Id;
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes);
updateModel.Compositions = MapCompositions(requestModel.Compositions);
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
return updateModel;
}
@@ -72,8 +74,8 @@ internal sealed class DocumentTypeEditingPresentationFactory : ContentTypeEditin
.DistinctBy(t => t.DocumentType.Id)
.ToDictionary(t => t.DocumentType.Id, t => t.SortOrder));
private IEnumerable<Composition> MapCompositions(IEnumerable<DocumentTypeComposition> documentTypeCompositions)
=> MapCompositions(documentTypeCompositions
private IDictionary<Guid, ViewModels.ContentType.CompositionType> CompositionTypesByKey(IEnumerable<DocumentTypeComposition> documentTypeCompositions)
=> documentTypeCompositions
.DistinctBy(c => c.DocumentType.Id)
.ToDictionary(c => c.DocumentType.Id, c => c.CompositionType));
.ToDictionary(c => c.DocumentType.Id, c => c.CompositionType);
}
@@ -26,7 +26,7 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
.Result),
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework
new DateTimeOffset(contentVersion.VersionDate),
contentVersion.CurrentPublishedVersion,
contentVersion.CurrentDraftVersion,
contentVersion.PreventCleanup);
@@ -23,11 +23,13 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
>(requestModel);
createModel.Key = requestModel.Id;
createModel.ContainerKey = requestModel.Parent?.Id;
createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
createModel.Compositions = MapCompositions(requestModel.Compositions);
createModel.ListView = requestModel.Collection?.Id;
IDictionary<Guid, ViewModels.ContentType.CompositionType> compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions);
createModel.Compositions = MapCompositions(compositionTypesByKey);
createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey);
return createModel;
}
@@ -42,7 +44,7 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
>(requestModel);
updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes);
updateModel.Compositions = MapCompositions(requestModel.Compositions);
updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions));
updateModel.ListView = requestModel.Collection?.Id;
return updateModel;
@@ -56,8 +58,8 @@ internal sealed class MediaTypeEditingPresentationFactory : ContentTypeEditingPr
.DistinctBy(t => t.MediaType.Id)
.ToDictionary(t => t.MediaType.Id, t => t.SortOrder));
private IEnumerable<Composition> MapCompositions(IEnumerable<MediaTypeComposition> documentTypeCompositions)
=> MapCompositions(documentTypeCompositions
private IDictionary<Guid, ViewModels.ContentType.CompositionType> CompositionTypesByKey(IEnumerable<MediaTypeComposition> documentTypeCompositions)
=> documentTypeCompositions
.DistinctBy(c => c.MediaType.Id)
.ToDictionary(c => c.MediaType.Id, c => c.CompositionType));
.ToDictionary(c => c.MediaType.Id, c => c.CompositionType);
}
@@ -83,24 +83,10 @@ public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeModel,
? CompositionType.Inheritance
: CompositionType.Composition;
protected static IEnumerable<T> MapNestedCompositions<T>(IEnumerable<IContentTypeComposition> directCompositions, int contentTypeParentId, Func<ReferenceByIdModel, CompositionType, T> contentTypeCompositionFactory)
{
var allCompositions = new List<T>();
foreach (var composition in directCompositions)
{
CompositionType compositionType = CalculateCompositionType(contentTypeParentId, composition);
T contentTypeComposition = contentTypeCompositionFactory(new ReferenceByIdModel(composition.Key), compositionType);
allCompositions.Add(contentTypeComposition);
// When we have composition inheritance, we have to find all ancestor compositions recursively
if (compositionType == CompositionType.Inheritance && composition.ContentTypeComposition.Any())
{
var nestedCompositions = MapNestedCompositions(composition.ContentTypeComposition, composition.ParentId, contentTypeCompositionFactory);
allCompositions.AddRange(nestedCompositions);
}
}
return allCompositions;
}
protected static IEnumerable<T> MapCompositions<T>(IEnumerable<IContentTypeComposition> directCompositions, int contentTypeParentId, Func<ReferenceByIdModel, CompositionType, T> contentTypeCompositionFactory)
=> directCompositions
.Select(composition => contentTypeCompositionFactory(
new ReferenceByIdModel(composition.Key),
CalculateCompositionType(contentTypeParentId, composition)))
.ToArray();
}
@@ -41,7 +41,7 @@ public class DocumentTypeMapDefinition : ContentTypeMapDefinition<IContentType,
new DocumentTypeSort { DocumentType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder })
.OrderBy(ct => ct.SortOrder)
.ToArray() ?? Enumerable.Empty<DocumentTypeSort>();
target.Compositions = MapNestedCompositions(
target.Compositions = MapCompositions(
source.ContentTypeComposition,
source.ParentId,
(referenceByIdModel, compositionType) => new DocumentTypeComposition
@@ -40,7 +40,7 @@ public class MediaTypeMapDefinition : ContentTypeMapDefinition<IMediaType, Media
target.AllowedMediaTypes = source.AllowedContentTypes?.Select(ct =>
new MediaTypeSort { MediaType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder })
.ToArray() ?? Enumerable.Empty<MediaTypeSort>();
target.Compositions = MapNestedCompositions(
target.Compositions = MapCompositions(
source.ContentTypeComposition,
source.ParentId,
(referenceByIdModel, compositionType) => new MediaTypeComposition
@@ -32,7 +32,7 @@ public class MemberTypeMapDefinition : ContentTypeMapDefinition<IMemberType, Mem
target.IsElement = source.IsElement;
target.Containers = MapPropertyTypeContainers(source);
target.Properties = MapPropertyTypes(source);
target.Compositions = MapNestedCompositions(
target.Compositions = MapCompositions(
source.ContentTypeComposition,
source.ParentId,
(referenceByIdModel, compositionType) => new MemberTypeComposition
@@ -36,5 +36,7 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions<Json
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver;
options.JsonSerializerOptions.MaxDepth = 64; // Ensures the maximum possible value is used, in particular to support handling as best we can levels of nested blocks.
}
}
@@ -37,7 +37,7 @@ public class ContentVersionMeta
public int UserId { get; }
public DateTime VersionDate { get; }
public DateTime VersionDate { get; private set; }
public bool CurrentPublishedVersion { get; }
@@ -47,5 +47,7 @@ public class ContentVersionMeta
public string? Username { get; }
public void SpecifyVersionDateKind(DateTimeKind kind) => VersionDate = DateTime.SpecifyKind(VersionDate, kind);
public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}";
}
@@ -322,6 +322,15 @@ internal abstract class ContentTypeEditingServiceBase<TContentType, TContentType
// get the content type keys we want to use for compositions
Guid[] compositionKeys = KeysForCompositionTypes(model, CompositionType.Composition);
// if the content type keys are already set as compositions, don't perform any additional validation
// - this covers an edge case where compositions are configured for a content type before child content types are created
if (contentType is not null && contentType.ContentTypeComposition
.Select(c => c.Key)
.ContainsAll(compositionKeys))
{
return ContentTypeOperationStatus.Success;
}
// verify that all compositions keys are allowed
Guid[] allowedCompositionKeys = _contentTypeService.GetAvailableCompositeContentTypes(contentType, allContentTypeCompositions, isElement: model.IsElement)
.Results
@@ -39,8 +39,11 @@ internal class ContentBaseFactory
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
content.Published = dto.Published;
content.Edited = dto.Edited;
@@ -52,7 +55,7 @@ internal class ContentBaseFactory
content.PublishedVersionId = publishedVersionDto.Id;
if (dto.Published)
{
content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate;
content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local);
content.PublishName = publishedVersionDto.ContentVersionDto.Text;
content.PublisherId = publishedVersionDto.ContentVersionDto.UserId;
}
@@ -71,7 +74,7 @@ internal class ContentBaseFactory
}
/// <summary>
/// Builds an IMedia item from a dto and content type.
/// Builds a Media item from a dto and content type.
/// </summary>
public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType)
{
@@ -97,8 +100,8 @@ internal class ContentBaseFactory
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
// reset dirty initial properties (U4-1946)
content.ResetDirtyProperties(false);
@@ -111,7 +114,7 @@ internal class ContentBaseFactory
}
/// <summary>
/// Builds an IMedia item from a dto and content type.
/// Builds a Member item from a dto and member type.
/// </summary>
public static Member BuildEntity(MemberDto dto, IMemberType? contentType)
{
@@ -126,7 +129,9 @@ internal class ContentBaseFactory
content.Id = dto.NodeId;
content.SecurityStamp = dto.SecurityStampToken;
content.EmailConfirmedDate = dto.EmailConfirmedDate;
content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue
? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local)
: null;
content.PasswordConfiguration = dto.PasswordConfig;
content.Key = nodeDto.UniqueId;
content.VersionId = contentVersionDto.Id;
@@ -140,14 +145,20 @@ internal class ContentBaseFactory
content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId;
content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local);
content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local);
content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default;
content.IsLockedOut = dto.IsLockedOut;
content.IsApproved = dto.IsApproved;
content.LastLoginDate = dto.LastLoginDate;
content.LastLockoutDate = dto.LastLockoutDate;
content.LastPasswordChangeDate = dto.LastPasswordChangeDate;
content.LastLockoutDate = dto.LastLockoutDate.HasValue
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
: null;
content.LastLoginDate = dto.LastLoginDate.HasValue
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
: null;
content.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
: null;
// reset dirty initial properties (U4-1946)
content.ResetDirtyProperties(false);
@@ -186,7 +197,7 @@ internal class ContentBaseFactory
new ContentScheduleDto
{
Action = x.Action.ToString(),
Date = x.Date,
Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local),
NodeId = entity.Id,
LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false),
Id = x.Id,
@@ -261,7 +272,7 @@ internal class ContentBaseFactory
UserId = entity.CreatorId,
Text = entity.Name,
NodeObjectType = objectType,
CreateDate = entity.CreateDate,
CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local),
};
return dto;
@@ -275,7 +286,7 @@ internal class ContentBaseFactory
{
Id = entity.VersionId,
NodeId = entity.Id,
VersionDate = entity.UpdateDate,
VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local),
UserId = entity.WriterId,
Current = true, // always building the current one
Text = entity.Name,
@@ -39,16 +39,25 @@ internal static class UserFactory
user.Language = dto.UserLanguage;
user.SecurityStamp = dto.SecurityStampToken;
user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0;
user.LastLockoutDate = dto.LastLockoutDate;
user.LastLoginDate = dto.LastLoginDate;
user.LastPasswordChangeDate = dto.LastPasswordChangeDate;
user.CreateDate = dto.CreateDate;
user.UpdateDate = dto.UpdateDate;
user.Avatar = dto.Avatar;
user.EmailConfirmedDate = dto.EmailConfirmedDate;
user.InvitedDate = dto.InvitedDate;
user.Kind = (UserKind)dto.Kind;
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
user.LastLockoutDate = dto.LastLockoutDate.HasValue
? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local)
: null;
user.LastLoginDate = dto.LastLoginDate.HasValue
? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local)
: null;
user.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue
? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local)
: null;
user.CreateDate = DateTime.SpecifyKind(dto.CreateDate, DateTimeKind.Local);
user.UpdateDate = DateTime.SpecifyKind(dto.UpdateDate, DateTimeKind.Local);
// reset dirty initial properties (U4-1946)
user.ResetDirtyProperties(false);
@@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
}
public void CleanLogs(int maximumAgeOfLogsInMinutes)
@@ -104,12 +104,12 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
totalRecords = page.TotalItems;
var items = page.Items.Select(
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList();
dto => new AuditItem(dto.NodeId, Enum<AuditType>.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList();
// map the DateStamp
for (var i = 0; i < items.Count; i++)
{
items[i].CreateDate = page.Items[i].Datestamp;
items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local);
}
return items;
@@ -149,7 +149,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
LogDto? dto = Database.First<LogDto>(sql);
return dto == null
? null
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp);
: new AuditItem(dto.NodeId, Enum<AuditType>.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local));
}
protected override IEnumerable<IAuditItem> PerformGetAll(params int[]? ids) => throw new NotImplementedException();
@@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase<int, IAuditItem>, IAuditRe
List<LogDto>? dtos = Database.Fetch<LogDto>(sql);
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList();
return dtos.Select(x => new AuditItem(x.NodeId, Enum<AuditType>.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList();
}
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
@@ -400,15 +400,17 @@ public class DocumentRepository : ContentRepositoryBase<int, IContent, DocumentR
{
foreach (ContentVariation v in contentVariation)
{
content.SetCultureInfo(v.Culture, v.Name, v.Date);
content.SetCultureInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
}
}
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
if (content.PublishedState is PublishedState.Published && content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation))
{
foreach (ContentVariation v in contentVariation)
{
content.SetPublishInfo(v.Culture, v.Name, v.Date);
content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local));
}
}
@@ -1,3 +1,4 @@
using System.Data;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
@@ -98,6 +99,16 @@ internal class DocumentVersionRepository : IDocumentVersionRepository
Page<ContentVersionMeta>? page =
_scopeAccessor.AmbientScope?.Database.Page<ContentVersionMeta>(pageIndex + 1, pageSize, query);
// Dates stored in the database are local server time, but for SQL Server, will be considered
// as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset.
if (page is not null)
{
foreach (ContentVersionMeta item in page.Items)
{
item.SpecifyVersionDateKind(DateTimeKind.Local);
}
}
totalRecords = page?.TotalItems ?? 0;
return page?.Items;
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Caching.Hybrid;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
@@ -115,12 +115,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService
// When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this.
// Similarly, when a branch is published, next time the content is requested, the parent will be published,
// this works because we don't cache null values.
if (preview is false && contentCacheNode is not null)
if (preview is false && contentCacheNode is not null && HasPublishedAncestorPath(contentCacheNode.Key) is false)
{
if (HasPublishedAncestorPath(contentCacheNode.Key) is false)
{
return null;
}
// Careful not to early return here. We need to complete the scope even if returning null.
contentCacheNode = null;
}
scope.Complete();
+4 -4
View File
@@ -88,7 +88,7 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"typescript-json-schema": "^0.65.1",
"vite": "^6.2.5",
"vite": "^6.2.6",
"vite-plugin-static-copy": "^2.2.0",
"vite-tsconfig-paths": "^5.1.4",
"web-component-analyzer": "^2.0.0"
@@ -16880,9 +16880,9 @@
}
},
"node_modules/vite": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -275,7 +275,7 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"typescript-json-schema": "^0.65.1",
"vite": "^6.2.5",
"vite": "^6.2.6",
"vite-plugin-static-copy": "^2.2.0",
"vite-tsconfig-paths": "^5.1.4",
"web-component-analyzer": "^2.0.0"
@@ -17,6 +17,8 @@ import {
UmbRequestReloadStructureForEntityEvent,
} from '@umbraco-cms/backoffice/entity-action';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document-type';
import { CompositionTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface UmbContentTypeWorkspaceContextArgs extends UmbEntityDetailWorkspaceContextArgs {}
@@ -95,6 +97,12 @@ export abstract class UmbContentTypeWorkspaceContextBase<
this.setUnique(data.unique);
this.setIsNew(true);
this._data.setPersisted(data);
if (!args.preset && args.parent.entityType === UMB_DOCUMENT_TYPE_ENTITY_TYPE && args.parent.unique) {
this.setCompositions([
{ contentType: { unique: args.parent.unique }, compositionType: CompositionTypeModel.INHERITANCE },
]);
}
}
this.loading.removeState(LOADING_STATE_UNIQUE);
@@ -10,7 +10,12 @@ export const manifests: Array<UmbExtensionManifest | UmbExtensionManifestKind> =
alias: 'Umb.EntityAction.DocumentType.Create',
name: 'Create Document Type Entity Action',
weight: 1200,
forEntityTypes: [UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE],
api: () => import('./create.action.js'),
forEntityTypes: [
UMB_DOCUMENT_TYPE_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE,
],
meta: {
icon: 'icon-add',
label: '#actions_create',
@@ -1,6 +1,8 @@
import { UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS } from '../../../tree/index.js';
import {
UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN,
UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_ELEMENT,
UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_TEMPLATE,
type UmbCreateDocumentTypeWorkspacePresetType,
} from '../../../paths.js';
import type { UmbDocumentTypeEntityTypeUnion } from '../../../entity.js';
@@ -9,8 +11,10 @@ import { html, customElement, map } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree';
const CREATE_FOLDER_PRESET = 'folder';
// Include the types from the DocumentTypeWorkspacePresetType + folder.
type OptionsPresetType = UmbCreateDocumentTypeWorkspacePresetType | 'folder' | null;
type OptionsPresetType = UmbCreateDocumentTypeWorkspacePresetType | typeof CREATE_FOLDER_PRESET | null;
/** @deprecated No longer used internally. This will be removed in Umbraco 17. [LK] */
@customElement('umb-document-type-create-options-modal')
@@ -30,13 +34,13 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement<Um
icon: 'icon-document',
},
{
preset: 'template',
preset: UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_TEMPLATE,
label: this.localize.term('create_documentTypeWithTemplate'),
description: this.localize.term('create_documentTypeWithTemplateDescription'),
icon: 'icon-document-html',
},
{
preset: 'element',
preset: UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_ELEMENT,
label: this.localize.term('create_elementType'),
description: this.localize.term('create_elementTypeDescription'),
icon: 'icon-plugin',
@@ -49,7 +53,7 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement<Um
// icon: 'icon-plugin',
// },
{
preset: 'folder',
preset: CREATE_FOLDER_PRESET,
label: this.localize.term('create_folder'),
description: this.localize.term('create_folderDescription'),
icon: 'icon-folder',
@@ -67,9 +71,10 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement<Um
meta: { icon: '', label: '', folderRepositoryAlias: UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS },
});
}
async #onClick(presetAlias: OptionsPresetType) {
switch (presetAlias) {
case 'folder': {
case CREATE_FOLDER_PRESET: {
try {
await this.#createFolderAction?.execute();
this._submitModal();
@@ -9,18 +9,19 @@ import type { UmbDocumentTypeDetailModel } from '../../types.js';
import { UMB_DOCUMENT_TYPE_ENTITY_TYPE, UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../constants.js';
import { UmbDocumentTypeWorkspaceEditorElement } from './document-type-workspace-editor.element.js';
import { UMB_DOCUMENT_TYPE_WORKSPACE_ALIAS } from './constants.js';
import { CompositionTypeModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbContentTypeWorkspaceContextBase } from '@umbraco-cms/backoffice/content-type';
import { UmbTemplateDetailRepository } from '@umbraco-cms/backoffice/template';
import {
UmbWorkspaceIsNewRedirectController,
UmbWorkspaceIsNewRedirectControllerAlias,
} from '@umbraco-cms/backoffice/workspace';
import type { UmbContentTypeSortModel, UmbContentTypeWorkspaceContext } from '@umbraco-cms/backoffice/content-type';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import type { UmbPathPatternTypeAsEncodedParamsType } from '@umbraco-cms/backoffice/router';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
import type { UmbRoutableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import type { UmbPathPatternTypeAsEncodedParamsType } from '@umbraco-cms/backoffice/router';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbTemplateDetailRepository } from '@umbraco-cms/backoffice/template';
type DetailModelType = UmbDocumentTypeDetailModel;
export class UmbDocumentTypeWorkspaceContext
@@ -148,6 +149,18 @@ export class UmbDocumentTypeWorkspaceContext
break;
}
if (parent.unique && parent.entityType === UMB_DOCUMENT_TYPE_ENTITY_TYPE) {
preset = {
...preset,
compositions: [
{
contentType: { unique: parent.unique },
compositionType: CompositionTypeModel.INHERITANCE,
},
],
};
}
this.createScaffold({ parent, preset });
}
+4 -4
View File
@@ -11,7 +11,7 @@
"@umbraco-cms/backoffice": "15.3.0",
"msw": "^2.7.0",
"typescript": "^5.7.3",
"vite": "^6.2.5",
"vite": "^6.2.6",
"vite-tsconfig-paths": "^5.1.4"
},
"engines": {
@@ -4292,9 +4292,9 @@
}
},
"node_modules/vite": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
"version": "6.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -19,7 +19,7 @@
"@umbraco-cms/backoffice": "15.3.0",
"msw": "^2.7.0",
"typescript": "^5.7.3",
"vite": "^6.2.5",
"vite": "^6.2.6",
"vite-tsconfig-paths": "^5.1.4"
},
"msw": {
@@ -0,0 +1,117 @@
import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
// Content Name
const contentName = 'ContentName';
// Document Type
const documentTypeName = 'DocumentTypeName';
let documentTypeId = null;
const documentTypeGroupName = 'DocumentGroup';
// Block Grid
const blockGridName = 'BlockGridName';
let blockGridId = null;
// Element Type
const blockName = 'BlockName';
let elementTypeId = null;
const elementGroupName = 'ElementGroup';
// Property Editor
const propertyEditorName = 'ProperyEditorInBlockName';
let propertyEditorId = null;
const optionValues = ['testOption1', 'testOption2'];
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockName);
await umbracoApi.dataType.ensureNameNotExists(blockGridName);
});
test('can not publish a block grid with a mandatory radiobox without a value', async ({umbracoApi, umbracoUi}) => {
// Arrange
propertyEditorId = await umbracoApi.dataType.createRadioboxDataType(propertyEditorName, optionValues);
elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true);
blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
// Do not select any radiobox values and the validation error appears
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
// Select a radiobox value and the validation error disappears
await umbracoUi.content.chooseRadioboxOption(optionValues[0]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
});
test('can not publish a block grid with a mandatory checkbox list without a value', async ({umbracoApi, umbracoUi}) => {
// Arrange
propertyEditorId = await umbracoApi.dataType.createCheckboxListDataType(propertyEditorName, optionValues);
elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true);
blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
// Do not select any checkbox list values and the validation error appears
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
// Select a checkbox list value and the validation error disappears
await umbracoUi.content.chooseCheckboxListOption(optionValues[0]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
});
test('can not publish a block grid with a mandatory dropdown without a value', async ({umbracoApi, umbracoUi}) => {
// Arrange
propertyEditorId = await umbracoApi.dataType.createDropdownDataType(propertyEditorName, false, optionValues);
elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true);
blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
// Do not select any dropdown values and the validation error appears
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
// Select a dropdown value and the validation error disappears
await umbracoUi.content.chooseDropdownOption([optionValues[0]]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
});
@@ -32,9 +32,10 @@ test.beforeEach(async ({umbracoApi}) => {
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.language.ensureIsoCodeNotExists('da');
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockName);
await umbracoApi.dataType.ensureNameNotExists(blockGridName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('invariant document type with invariant block grid with invariant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
@@ -63,34 +64,25 @@ test('invariant document type with invariant block grid with invariant block wit
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
});
test('invariant document type with invariant block grid with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
test('can not create unsupported invariant document type with invariant block grid with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
// Arrange
elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, false);
blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockGridName, blockGridId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
await umbracoUi.content.enterTextstring(textStringText);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
await umbracoUi.reloadPage();
await umbracoUi.content.goToBlockGridBlockWithName(documentTypeGroupName, blockGridName, blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
});
// Remove fixme when this test works. Currently, the textstring value is not saved when saving / publishing the document
test.fixme('invariant document type with invariant block grid with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
test('can not create unsupported invariant document type with invariant block grid with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
// Arrange
elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, true);
blockGridId = await umbracoApi.dataType.createBlockGridWithABlockAndAllowAtRoot(blockGridName, elementTypeId, true);
@@ -98,22 +90,15 @@ test.fixme('invariant document type with invariant block grid with variant block
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName)
await umbracoUi.content.enterTextstring(textStringText);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
await umbracoUi.reloadPage();
await umbracoUi.content.goToBlockGridBlockWithName(documentTypeGroupName, blockGridName, blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
});
test('variant document type with variant block grid with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
@@ -195,4 +180,4 @@ test('variant document type with invariant block grid with variant block with an
await umbracoUi.reloadPage();
await umbracoUi.content.goToBlockGridBlockWithName(documentTypeGroupName, blockGridName, blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
});
});
@@ -0,0 +1,117 @@
import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
// Content Name
const contentName = 'ContentName';
// Document Type
const documentTypeName = 'DocumentTypeName';
let documentTypeId = null;
const documentTypeGroupName = 'DocumentGroup';
// Block List
const blockListName = 'BlockListName';
let blockListId = null;
// Element Type
const blockName = 'BlockName';
let elementTypeId = null;
const elementGroupName = 'ElementGroup';
// Property Editor
const propertyEditorName = 'ProperyEditorInBlockName';
let propertyEditorId = null;
const optionValues = ['testOption1', 'testOption2'];
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockName);
await umbracoApi.dataType.ensureNameNotExists(blockListName);
});
test('can not publish a block list with a mandatory radiobox without a value', async ({umbracoApi, umbracoUi}) => {
// Arrange
propertyEditorId = await umbracoApi.dataType.createRadioboxDataType(propertyEditorName, optionValues);
elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true);
blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
// Do not select any radiobox values and the validation error appears
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
// Select a radiobox value and the validation error disappears
await umbracoUi.content.chooseRadioboxOption(optionValues[0]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
});
test('can not publish a block list with a mandatory checkbox list without a value', async ({umbracoApi, umbracoUi}) => {
// Arrange
propertyEditorId = await umbracoApi.dataType.createCheckboxListDataType(propertyEditorName, optionValues);
elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true);
blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
// Do not select any checkbox list values and the validation error appears
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
// Select a checkbox list value and the validation error disappears
await umbracoUi.content.chooseCheckboxListOption(optionValues[0]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
});
test('can not publish a block list with a mandatory dropdown without a value', async ({umbracoApi, umbracoUi}) => {
// Arrange
propertyEditorId = await umbracoApi.dataType.createDropdownDataType(propertyEditorName, false, optionValues);
elementTypeId = await umbracoApi.documentType.createDefaultElementType(blockName, elementGroupName, propertyEditorName, propertyEditorId, true);
blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
// Do not select any dropdown values and the validation error appears
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
// Select a dropdown value and the validation error disappears
await umbracoUi.content.chooseDropdownOption([optionValues[0]]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
});
@@ -32,9 +32,10 @@ test.beforeEach(async ({umbracoApi}) => {
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.language.ensureIsoCodeNotExists('da');
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockName);
await umbracoApi.dataType.ensureNameNotExists(blockListName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('invariant document type with invariant block list with invariant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
@@ -63,57 +64,40 @@ test('invariant document type with invariant block list with invariant block wit
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
});
test('invariant document type with invariant block list with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
test('can not create unsupported invariant document type with invariant block list with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
// Arrange
elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, false);
blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
await umbracoUi.content.enterTextstring(textStringText);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
await umbracoUi.reloadPage();
await umbracoUi.content.goToBlockListBlockWithName(documentTypeGroupName, blockListName, blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
});
// Remove fixme when this test works. Currently the textstring value is is not saved when saving / publishing the document
test.fixme('invariant document type with invariant block list with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
test('can not create unsupported invariant document type with invariant block list with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
// Arrange
elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, true);
blockListId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, blockListName, blockListId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickAddBlockElementButton();
await umbracoUi.content.clickBlockElementWithName(blockName)
await umbracoUi.content.enterTextstring(textStringText);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
await umbracoUi.reloadPage();
await umbracoUi.content.goToBlockListBlockWithName(documentTypeGroupName, blockListName, blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
});
test('variant document type with variant block list with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
@@ -1,19 +1,22 @@
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const dataTypeName = 'Checkbox list';
const customDataTypeName = 'CustomCheckboxList';
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can create content with the checkbox list data type', async ({umbracoApi, umbracoUi}) => {
@@ -31,7 +34,7 @@ test('can create content with the checkbox list data type', async ({umbracoApi,
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
@@ -51,8 +54,8 @@ test('can publish content with the checkbox list data type', async ({umbracoApi,
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
@@ -60,7 +63,6 @@ test('can publish content with the checkbox list data type', async ({umbracoApi,
test('can create content with the custom checkbox list data type', async ({umbracoApi, umbracoUi}) => {
// Arrange
const customDataTypeName = 'CustomCheckboxList';
const optionValues = ['testOption1', 'testOption2'];
const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
@@ -73,13 +75,38 @@ test('can create content with the custom checkbox list data type', async ({umbra
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual([optionValues[0]]);
// Clean
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can not publish a mandatory checkbox list with an empty value', async ({umbracoApi, umbracoUi}) => {
// Arrange
const optionValues = ['testOption1', 'testOption2'];
const customDataTypeId = await umbracoApi.dataType.createCheckboxListDataType(customDataTypeName, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'Test Group', false, false, true);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
// Do not select any checkbox list values and the validation error appears
await umbracoUi.content.clickSaveAndPublishButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
// Select a checkbox list value and the validation error disappears
await umbracoUi.content.chooseCheckboxListOption(optionValues[0]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual([optionValues[0]]);
});
@@ -1,90 +1,116 @@
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const dataTypeNames = ['Dropdown', 'Dropdown multiple'];
const customDataTypeName = 'CustomDropdown';
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
for (const dataTypeName of dataTypeNames) {
test.describe(`${dataTypeName} tests`, () => {
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoUi.goToBackOffice();
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test(`can create content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
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);
expect(contentData.values).toEqual([]);
});
test(`can publish content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
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);
expect(contentData.values).toEqual([]);
});
test(`can create content with the custom ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const customDataTypeName = 'CustomDropdown';
const optionValues = ['testOption1', 'testOption2', 'testOption3'];
const selectedOptions = dataTypeName === 'Dropdown' ? [optionValues[0]] : optionValues;
const isMultiple = dataTypeName === 'Dropdown' ? false : true;
const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, isMultiple, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.chooseDropdownOption(selectedOptions);
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.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual(selectedOptions);
// Clean
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test(`can create content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Draft';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
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.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
});
test(`can publish content with the ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const expectedState = 'Published';
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
expect(contentData.values).toEqual([]);
});
test(`can create content with the custom ${dataTypeName} data type`, async ({umbracoApi, umbracoUi}) => {
// Arrange
const optionValues = ['testOption1', 'testOption2', 'testOption3'];
const selectedOptions = dataTypeName === 'Dropdown' ? [optionValues[0]] : optionValues;
const isMultiple = dataTypeName === 'Dropdown' ? false : true;
const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, isMultiple, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.chooseDropdownOption(selectedOptions);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual(selectedOptions);
});
}
test('can not publish a mandatory dropdown with an empty value', async ({umbracoApi, umbracoUi}) => {
// Arrange
const optionValues = ['testOption1', 'testOption2', 'testOption3'];
const customDataTypeId = await umbracoApi.dataType.createDropdownDataType(customDataTypeName, false, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'Test Group', false, false, true);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
// Do not select any dropdown values and the validation error appears
await umbracoUi.content.clickSaveAndPublishButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
// Select a dropdown value and the validation error disappears
await umbracoUi.content.chooseDropdownOption([optionValues[0]]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual([optionValues[0]]);
});
@@ -1,4 +1,4 @@
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const dataTypeName = 'Media Picker';
@@ -8,7 +8,7 @@ const mediaFileName = 'TestMediaFileForContent';
const mediaTypeName = 'File';
let mediaFileId = '';
test.beforeEach(async ({umbracoApi, umbracoUi}) => {
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.media.ensureNameNotExists(mediaFileName);
@@ -39,7 +39,7 @@ test('can create content with the media picker data type', {tag: '@smoke'}, asyn
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
@@ -68,7 +68,8 @@ test('can publish content with the media picker data type', async ({umbracoApi,
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
@@ -93,7 +94,7 @@ test('can remove a media picker in the content', async ({umbracoApi, umbracoUi})
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values).toEqual([]);
@@ -128,3 +129,34 @@ test('can limit the media picker in the content by setting the start node', asyn
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can not publish a mandatory media picker with an empty value', async ({umbracoApi, umbracoUi}) => {
// Arrange
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, 'Test Group', false, false, true);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
// Do not pick any media and the validation error appears
await umbracoUi.content.clickSaveAndPublishButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
// Pick a media value and the validation error disappears
await umbracoUi.content.clickChooseButtonAndSelectMediaWithName(mediaFileName);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(dataTypeName));
expect(contentData.values[0].value[0].mediaKey).toEqual(mediaFileId);
expect(contentData.values[0].value[0].mediaTypeAlias).toEqual(mediaTypeName);
expect(contentData.values[0].value[0].focalPoint).toBeNull();
expect(contentData.values[0].value[0].crops).toEqual([]);
});
@@ -1,18 +1,22 @@
import {ConstantHelper, test, AliasHelper} from '@umbraco/playwright-testhelpers';
import {ConstantHelper, test, AliasHelper, NotificationConstantHelper} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const dataTypeName = 'Radiobox';
const customDataTypeName = 'CustomRadiobox';
const optionValues = ['testOption1', 'testOption2'];
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can create content with the radiobox data type', async ({umbracoApi, umbracoUi}) => {
@@ -31,7 +35,7 @@ test('can create content with the radiobox data type', async ({umbracoApi, umbra
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.created);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
@@ -52,7 +56,8 @@ test('can publish content with the radiobox data type', async ({umbracoApi, umbr
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationsHaveCount(2);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.variants[0].state).toBe(expectedState);
@@ -61,8 +66,6 @@ test('can publish content with the radiobox data type', async ({umbracoApi, umbr
test('can create content with the custom radiobox data type', async ({umbracoApi, umbracoUi}) => {
// Arrange
const customDataTypeName = 'CustomRadiobox';
const optionValues = ['testOption1', 'testOption2'];
const customDataTypeId = await umbracoApi.dataType.createRadioboxDataType(customDataTypeName, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
@@ -75,13 +78,37 @@ test('can create content with the custom radiobox data type', async ({umbracoApi
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessNotificationVisible();
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual(optionValues[0]);
// Clean
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can not publish mandatory radiobox with an empty value', async ({umbracoApi, umbracoUi}) => {
// Arrange
const customDataTypeId = await umbracoApi.dataType.createRadioboxDataType(customDataTypeName, optionValues);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'Test Group', false, false, true);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
// Do not select any radiobox values and the validation error appears
await umbracoUi.content.clickSaveAndPublishButton();
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
// Select a radiobox value and the validation error disappears
await umbracoUi.content.chooseRadioboxOption(optionValues[0]);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.emptyValue, false);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].alias).toEqual(AliasHelper.toAlias(customDataTypeName));
expect(contentData.values[0].value).toEqual(optionValues[0]);
});
@@ -81,5 +81,4 @@ test('can remove a tag in the content', async ({umbracoApi, umbracoUi}) => {
expect(await umbracoApi.document.doesNameExist(contentName)).toBeTruthy();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values).toEqual([]);
});
});
@@ -32,9 +32,10 @@ test.beforeEach(async ({umbracoApi}) => {
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.language.ensureIsoCodeNotExists('da');
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.documentType.ensureNameNotExists(blockName);
await umbracoApi.dataType.ensureNameNotExists(tipTapName);
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
test('invariant document type with invariant tiptap RTE with invariant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
@@ -64,57 +65,41 @@ test('invariant document type with invariant tiptap RTE with invariant block wit
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
});
test('invariant document type with invariant tiptap RTE with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
test('can not create unsupported invariant document type with invariant tiptap RTE with variant block with an invariant textString', async ({umbracoApi, umbracoUi}) => {
// Arrange
elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, false);
tipTapId = await umbracoApi.dataType.createTipTapDataTypeWithABlock(tipTapName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, tipTapName, tipTapId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickInsertBlockButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
await umbracoUi.content.enterTextstring(textStringText);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
await umbracoUi.reloadPage();
await umbracoUi.content.clickBlockElementWithName(blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
});
// Remove fixme when this test works. Currently the textstring value is is not saved when saving / publishing the document
test.fixme('invariant document type with invariant tiptap RTE with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
test('can not create unsupported invariant document type with invariant tiptap RTE with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
// Arrange
elementTypeId = await umbracoApi.documentType.createDefaultElementTypeWithVaryByCulture(blockName, elementGroupName, textStringName, textStringDataTypeId, true, true);
tipTapId = await umbracoApi.dataType.createTipTapDataTypeWithABlock(tipTapName, elementTypeId);
documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, tipTapName, tipTapId, documentTypeGroupName);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
await umbracoUi.content.goToContentWithName(contentName);
// Act
await umbracoUi.content.clickInsertBlockButton();
await umbracoUi.content.clickBlockElementWithName(blockName);
await umbracoUi.content.enterTextstring(textStringText);
await umbracoUi.content.clickCreateModalButton();
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.isValidationMessageVisible(ConstantHelper.validationMessages.unsupportInvariantContentItemWithVariantBlocks);
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.saved);
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
await umbracoUi.reloadPage();
await umbracoUi.content.clickBlockElementWithName(blockName);
await umbracoUi.content.doesPropertyContainValue(textStringName, textStringText);
await umbracoUi.content.doesErrorNotificationHaveText(NotificationConstantHelper.error.documentCouldNotBePublished);
});
test('variant document type with variant tiptap RTE with variant block with an variant textString', async ({umbracoApi, umbracoUi}) => {
@@ -544,6 +544,35 @@ internal sealed partial class ContentTypeEditingServiceTests
});
}
[Test]
public async Task Can_Create_Child_To_Content_Type_With_Composition()
{
var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!;
var parentContentType = (await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Parent",
compositions: [new Composition { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]),
Constants.Security.SuperUserKey)).Result!;
var result = await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Child",
compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]),
Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
// Ensure it's actually persisted
var childContentType = await ContentTypeService.GetAsync(result.Result!.Key);
Assert.IsNotNull(childContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Child", childContentType.Name);
Assert.AreEqual(1, childContentType.ContentTypeComposition.Count());
Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key);
});
}
[Test]
public async Task Cannot_Be_Both_Parent_And_Composition()
{
@@ -691,6 +720,32 @@ internal sealed partial class ContentTypeEditingServiceTests
Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status);
}
[Test]
public async Task Cannot_Have_Same_Key_For_Inheritance_And_Parent()
{
var parentModel = ContentTypeCreateModel("Parent");
var parent = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result;
Assert.IsNotNull(parent);
Composition[] composition =
{
new()
{
CompositionType = CompositionType.Inheritance, Key = parent.Key,
}
};
var childModel = ContentTypeCreateModel(
"Child",
containerKey: parent.Key,
compositions: composition);
var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status);
}
[Test]
public async Task Cannot_Use_As_ParentKey()
{
@@ -601,6 +601,91 @@ internal sealed partial class ContentTypeEditingServiceTests
Assert.AreEqual(567, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays);
}
[Test]
public async Task Can_Reapply_Compositions_For_Content_Type_With_Children()
{
var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!;
var parentContentType = (await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Parent",
compositions: [new Composition { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]),
Constants.Security.SuperUserKey)).Result!;
var childContentType = (await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Child",
compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]),
Constants.Security.SuperUserKey)).Result!;
var updateModel = ContentTypeUpdateModel(
"Parent Updated",
compositions: [new() { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]);
var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
// Ensure it's actually persisted
parentContentType = await ContentTypeService.GetAsync(parentContentType.Key);
Assert.IsNotNull(parentContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Parent Updated", parentContentType.Name);
Assert.AreEqual(1, parentContentType.ContentTypeComposition.Count());
Assert.AreEqual(compositionContentType.Key, parentContentType.ContentTypeComposition.Single().Key);
});
childContentType = await ContentTypeService.GetAsync(childContentType.Key);
Assert.IsNotNull(childContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Child", childContentType.Name);
Assert.AreEqual(1, childContentType.ContentTypeComposition.Count());
Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key);
});
}
[Test]
public async Task Can_Remove_Compositions_For_Content_Type_With_Children()
{
var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!;
var parentContentType = (await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Parent",
compositions: [new Composition { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]),
Constants.Security.SuperUserKey)).Result!;
var childContentType = (await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Child",
compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]),
Constants.Security.SuperUserKey)).Result!;
var updateModel = ContentTypeUpdateModel("Parent Updated", compositions: []);
var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey);
Assert.IsTrue(result.Success);
// Ensure it's actually persisted
parentContentType = await ContentTypeService.GetAsync(parentContentType.Key);
Assert.IsNotNull(parentContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Parent Updated", parentContentType.Name);
Assert.IsEmpty(parentContentType.ContentTypeComposition);
});
childContentType = await ContentTypeService.GetAsync(childContentType.Key);
Assert.IsNotNull(childContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Child", childContentType.Name);
Assert.AreEqual(1, childContentType.ContentTypeComposition.Count());
Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key);
});
}
[TestCase(false)]
[TestCase(true)]
public async Task Cannot_Move_Properties_To_Non_Existing_Containers(bool isElement)
@@ -826,4 +911,43 @@ internal sealed partial class ContentTypeEditingServiceTests
Assert.IsFalse(result.Success);
Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerType, result.Status);
}
[Test]
public async Task Cannot_Add_Compositions_For_Content_Type_With_Children()
{
var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!;
var parentContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!;
var childContentType = (await ContentTypeEditingService.CreateAsync(
ContentTypeCreateModel(
"Child",
compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]),
Constants.Security.SuperUserKey)).Result!;
var updateModel = ContentTypeUpdateModel(
"Parent Updated",
compositions: [new() { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]);
var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey);
Assert.IsFalse(result.Success);
// Ensure nothing was persisted
parentContentType = await ContentTypeService.GetAsync(parentContentType.Key);
Assert.IsNotNull(parentContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Parent", parentContentType.Name);
Assert.AreEqual(0, parentContentType.ContentTypeComposition.Count());
});
childContentType = await ContentTypeService.GetAsync(childContentType.Key);
Assert.IsNotNull(childContentType);
Assert.Multiple(() =>
{
Assert.AreEqual("Child", childContentType.Name);
Assert.AreEqual(1, childContentType.ContentTypeComposition.Count());
Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key);
});
}
}
@@ -0,0 +1,80 @@
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using NUnit.Framework;
using Umbraco.Cms.Api.Common.Serialization;
using Umbraco.Cms.Api.Management.Serialization;
using Umbraco.Cms.Tests.UnitTests.TestHelpers;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Serialization;
[TestFixture]
public class BackOfficeSerializationTests
{
private JsonOptions jsonOptions;
[SetUp]
public void SetupOptions()
{
var typeInfoResolver = new UmbracoJsonTypeInfoResolver(TestHelper.GetTypeFinder());
var configurationOptions = new ConfigureUmbracoBackofficeJsonOptions(typeInfoResolver);
var options = new JsonOptions();
configurationOptions.Configure(global::Umbraco.Cms.Core.Constants.JsonOptionsNames.BackOffice, options);
jsonOptions = options;
}
[Test]
public void Will_Serialize_To_Camel_Case()
{
var objectToSerialize = new UnNestedJsonTestValue();
var json = JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions);
Assert.AreEqual("{\"stringValue\":\"theValue\"}", json);
}
// the limit is 64, but it seems like the functional limit is that minus 1
[TestCase(1, true, TestName = "Can_Serialize_At_Min_Depth(1)")]
[TestCase(48, true, TestName = "Can_Serialize_At_High_Depth(33)")]
[TestCase(63, true, TestName = "Can_Serialize_To_Max_Depth(63)")]
[TestCase(64, false, TestName = "Can_NOT_Serialize_Beyond_Max_Depth(64)")]
public void Can_Serialize_To_Max_Depth(int depth, bool shouldPass)
{
var objectToSerialize = CreateNestedObject(depth);
if (shouldPass)
{
var json = JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions);
Assert.IsNotEmpty(json);
}
else
{
Assert.Throws<JsonException>(() => JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions));
}
}
private static NestedJsonTestValue CreateNestedObject(int levels)
{
var root = new NestedJsonTestValue { Level = 1 };
var outer = root;
for (var i = 2; i <= levels; i++)
{
var inner = new NestedJsonTestValue { Level = i };
outer.Inner = inner;
outer = inner;
}
return root;
}
public class UnNestedJsonTestValue
{
public string StringValue { get; set; } = "theValue";
}
public class NestedJsonTestValue
{
public int Level { get; set; }
public NestedJsonTestValue? Inner { get; set; }
}
}