Merge branch 'v15/dev' into v16/dev

This commit is contained in:
Jacob Overgaard
2025-04-15 09:43:56 +02:00
36 changed files with 1243 additions and 273 deletions

View File

@@ -61,7 +61,7 @@ This guide describes each step to make your first contribution:
## Further contribution guides
- [Before you start](contributing-before-you-start.md)
- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
- [Finding your first issue: Up for grabs](contributing-first-issue.md)
- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
- [Unwanted changes](contributing-unwanted-changes.md)
- [Other ways to contribute](contributing-other-ways-to-contribute.md)

571
NOTICES.txt Normal file
View File

@@ -0,0 +1,571 @@
Third-Party Notices
===================
This file contains notices and attributions for third-party software used in the Umbraco CMS project.
It is not a license and does not grant any rights to use the third-party software.
Umbraco CMS is licensed under the MIT License, which can be found in the LICENSE file.
---
@openid/AppAuth-JS: An OpenID Connect and OAuth 2.0 client library for JavaScript
URL: https://github.com/openid/AppAuth-JS
License: Apache License, Version 2.0
Copyright: 2017 Google Inc.
---
AutoFixture: Write maintainable unit tests, faster
URL: https://github.com/AutoFixture/AutoFixture
License: MIT License
Copyright: 2013 Mark Seemann
---
Asp.Versioning.Mvc: A library for ASP.NET Core versioning
URL: https://github.com/dotnet/aspnet-api-versioning
License: MIT License
Copyright: .NET Foundation and contributors
---
Babel: A JavaScript compiler
URL: https://babeljs.io/
License: MIT License
Copyright: 2014-present Sebastian McKenzie and other contributors
---
BenchmarkDotNet: Powerful .NET library for benchmarking
URL: https://github.com/dotnet/BenchmarkDotNet
License: MIT License
Copyright: .NET Foundation and Contributors
---
Bogus: A simple and sane data generator for populating objects that supports different locales.
URL: https://github.com/bchavez/Bogus
License: MIT License
Copyright: 2015 Brian Chavez
---
CommandLineParser: Terse syntax C# command line parser for .NET
URL: https://github.com/commandlineparser/commandline
License: MIT License
Copyright: 2005-2015 Giacomo Stelluti Scala & Contributors
---
cross-env: A CLI tool to set environment variables across platforms
URL: https://github.com/kentcdodds/cross-env
License: MIT License
Copyright: 2017 Kent C. Dodds
---
Dazinator.Extensions.FileProviders: A library for file provider extensions
URL: https://github.com/dazinator/Dazinator.Extensions.FileProviders
License: MIT License
Copyright: 2016 Darrell
---
DOMPurify: A DOM-only XSS sanitizer for HTML, MathML and SVG
URL: https://github.com/cure53/DOMPurify
License: Apache License, Version 2.0
Copyright: 2025 Dr.-Ing. Mario Heiderich, Cure53
---
Element Internals Polyfill: A polyfill for the Element Internals API
URL: https://github.com/calebdwilliams/element-internals-polyfill
License: MIT License
Copyright: 2021 Caleb Williams
---
Eslint: A tool for identifying and reporting on patterns in JavaScript
URL: https://eslint.org/
License: MIT License
Copyright: OpenJS Foundation and other contributors
---
Examine: A search and indexing library for .NET
URL: https://github.com/Shazwazza/Examine
License: Microsoft Public License (Ms-PL)
Copyright: 2023 Shannon Deminick
---
Glob: A library for matching file paths using glob patterns
URL: https://github.com/isaacs/node-glob
License: ISC License
Copyright: 2009-2023 Isaac Z. Schlueter and Contributors
---
Globals: A library for managing global variables in JavaScript
URL: https://github.com/sindresorhus/globals
License: MIT License
Copyright: Sindre Sorhus
---
Html Agility Pack: An HTML parser for .NET
URL: https://html-agility-pack.net/
License: MIT License
Copyright: ZZZ Projects Inc.
---
ImageSharp: A cross-platform library for processing images in .NET
URL: https://github.com/SixLabors/ImageSharp
License: Apache License, Version 2.0 under the Six Labors Split License
Copyright: Six Labors
---
jsdiff: A JavaScript text differencing implementation
URL: https://github.com/kpdecker/jsdiff
License: BSD 3-Clause License
Copyright: 2009-2015 Kevin Decker <kpdecker@gmail.com>
---
JsonPatch.Net: A library for JSON Patch (RFC 6902) in .NET
URL: https://github.com/json-everything/json-everything
License: MIT License
Copyright: .NET Foundation and Contributors
---
K4os.Compression.LZ4: A fast LZ4 compression library for .NET
URL: https://github.com/MiloszKrajewski/K4os.Compression.LZ4
License: MIT License
Copyright: 2017 Milosz Krajewski
---
Lit: A simple library for building fast, lightweight web components
URL: https://lit.dev
License: BSD 3-Clause License
Copyright: 2020 Google LLC. All rights reserved.
---
Lucide: Beautiful & consistent icons for the web
URL: https://lucide.dev/
License: ISC License
Copyright: 2013-2022 Cole Bemis
Copyright: 2022 Lucide Contributors
---
Madge: A dependency graph generator for JavaScript
URL: https://github.com/pahen/madge
License: MIT License
Copyright: 2017 Patrik Henningsson
---
MailKit: A library for sending email in .NET
URL: https://github.com/jstedfast/MailKit
License: MIT License
Copyright: 2013-2024 .NET Foundation and Contributors
---
Markdown: A library for parsing and compiling Markdown
URL: https://github.com/hey-red/Markdown
License: MIT License
Copyright: 2018 red
---
marked: A markdown parser and compiler
URL: https://marked.js.org/
License: MIT License
Copyright: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
Copyright: 2018+, MarkedJS (https://github.com/markedjs/)
---
Message Pack: The extremely fast MessagePack serializer for C#
URL: https://github.com/MessagePack-CSharp/MessagePack-CSharp
License: MIT License
Copyright: 2017 Yoshifumi Kawai and contributors
---
Miniprofiler: A mini profiler for .NET
URL: https://github.com/MiniProfiler/dotnet
License: MIT License
Copyright: .NET MiniProfiler Contributors
---
Monaco Editor: A browser-based code editor
URL: https://microsoft.github.io/monaco-editor/
License: MIT License
Copyright: 2016-present Microsoft Corporation
---
Moq: A mocking library for .NET
URL: https://github.com/moq/moq
License: BSD 3-Clause License
Copyright: 2007 Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
---
Mock Service Worker (MSW): A library for mocking API requests in JavaScript
URL: https://mswjs.io/
License: MIT License
Copyright: 2018present Artem Zakharchenko
---
NCrontab: A cron schedule parser for .NET
URL: https://github.com/atifaziz/NCrontab
License: Apache License, Version 2.0
Copyright: 2001 The OpenSymphony Group
Copyright: 2008 Atif Aziz
---
Nerdbank.GitVersioning: A library for versioning .NET projects
URL: https://github.com/dotnet/Nerdbank.GitVersioning
License: MIT License
Copyright: .NET Foundation and Contributors
---
NJsonSchema: A JSON schema validator for .NET
URL: https://github.com/RicoSuter/NJsonSchema
License: MIT License
Copyright: 2022 Rico Suter
---
NPoco: A micro ORM for .NET
URL: https://github.com/schotime/NPoco
License: Apache License, Version 2.0
Copyright: Schotime
---
NUnit: A unit testing framework for .NET
URL: https://github.com/nunit/nunit
License: MIT License
Copyright: Charlie Poole, Rob Prouse and Contributors
---
Open Web Components: A set of standards and libraries for building web components
URL: https://open-wc.org/
License: MIT License
Copyright: 2018 open-wc
---
Openapi-ts: The OpenAPI to TypeScript codegen
URL: https://github.com/hey-api/openapi-ts
License: MIT License
Copyright: Hey API
---
OpenIddict: A simple and flexible OpenID Connect server for ASP.NET Core
URL: https://github.com/openiddict/openiddict-core
License: Apache License, Version 2.0
Copyright: Kévin Chalet
---
Playwright: A Node.js library to automate browser testing
URL: https://playwright.dev/
License: Apache License, Version 2.0
Copyright: 2025 Microsoft Corporation
---
Playwright-msw: A library to wrap Mock Service Worker with Playwright
URL: https://github.com/valendres/playwright-msw
License: MIT License
Copyright: 2022 Peter Weller
---
Prettier: An opinionated code formatter
URL: https://prettier.io/
License: MIT License
Copyright: James Long and contributors
---
Remark-gfm: A GitHub Flavored Markdown plugin for Remark
URL: https://github.com/remarkjs/remark-gfm
License: MIT License
Copyright: Titus Wormer
---
Rollup: A module bundler for JavaScript
URL: https://rollupjs.org/
License: MIT License
Copyright: 2015-present Rollup contributors
---
Rollup Plugins: A collection of Rollup plugins
URL: https://github.com/rollup/plugins
License: MIT License
Copyright: 2019-present Rollup Plugins contributors
---
Rollup-plugin-esbuild: A Rollup plugin for using esbuild
URL: https://github.com/egoist/rollup-plugin-esbuild
License: MIT License
Copyright: 2020 EGOIST
---
Rollup-plugin-import-css: A Rollup plugin for importing CSS files
URL: https://github.com/jleeson/rollup-plugin-import-css
License: MIT License
Copyright: 2020 Jacob Leeson
---
rxjs: Reactive Extensions for JavaScript
URL: https://rxjs.dev/
License: Apache License, Version 2.0
Copyright: 2015-present Ben Lesh <ben@benlesh.com>, Google, Inc., Netflix, Inc., Microsoft Corp., and contributors
---
Serilog: A diagnostic logging library for .NET
URL: https://github.com/serilog/serilog
License: Apache License, Version 2.0
Copyright: Serilog Contributors
---
Simple Icons: A set of SVG icons for popular brands
URL: https://simpleicons.org/
License: CC0 1.0 Universal License
Copyright: Simple Icons Contributors
---
Storybook: A UI component explorer for Web Components
URL: https://storybook.js.org/
License: MIT License
Copyright: 2024 Storybook
---
StyleCop.Analyzers: Analyzers for StyleCop
URL: https://github.com/DotNetAnalyzers/StyleCopAnalyzers
License: MIT License
Copyright: Tunnel Vision Laboratories, LLC
---
SVGO: A tool for optimizing SVG files
URL: https://svgo.dev/
License: MIT License
Copyright: Kir Belevich
---
Swashbuckle.AspNetCore: A library for generating Swagger documentation for ASP.NET Core APIs
URL: https://github.com/domaindrivendev/Swashbuckle.AspNetCore
License: MIT License
Copyright: 2016 Richard Morris
---
Tiny Glob: A tiny globbing library for Node.js
URL: https://github.com/terkelg/tiny-glob
License: MIT License
Copyright: 2018 Terkel
---
Tiptap: A renderless rich-text editor for the web
URL: https://tiptap.dev/
License: MIT License
Copyright: 2025 Tiptap GmbH
---
Tsc-alias: A TypeScript compiler plugin for aliasing module paths
URL: https://github.com/justkey007/tsc-alias
License: MIT License
Copyright: 2018 Justkey
---
Typedoc: A documentation generator for TypeScript projects
URL: https://typedoc.org/
License: Apache License, Version 2.0
Copyright: Gerrit Birkeland and Contributors
---
Typescript: A typed superset of JavaScript that compiles to plain JavaScript
URL: https://www.typescriptlang.org/
License: Apache License, Version 2.0
Copyright: 2012-present Microsoft Corporation
---
Typescript-eslint: A set of tools for linting TypeScript code
URL: https://github.com/typescript-eslint/typescript-eslint
License: MIT License
Copyright: 2019 typescript-eslint and other contributors
---
Typescript-json-schema: A library for generating JSON schema from TypeScript types
URL: https://github.com/YousefED/typescript-json-schema
License: BSD 3-Clause License
Copyright: 2016 typescript-json-schema contributors
---
Umbraco.Code: Provides code-level tools for Umbraco
URL: https://github.com/umbraco/Umbraco-Code
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco.GitVersioning.Extensions: Utilities for Nerdbank.GitVersioning
URL: https://github.com/umbraco/Umbraco.GitVersioning.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco.JsonSchema.Extensions: Utilities for JSON schema generation
URL: https://github.com/umbraco/Umbraco.JsonSchema.Extensions
License: MIT License
Copyright: 2005-present Umbraco A/S
---
Umbraco UI Library: A set of UI components for building web applications
URL: https://uui.umbraco.com/
License: MIT License
Copyright: 2005-present Umbraco A/S
---
uuid: A library for generating unique identifiers
URL: https://github.com/uuidjs/uuid
License: MIT License
Copyright: 2010-2020 Robert Kieffer and other contributors
---
Vite: A fast build tool and development server for modern web projects
URL: https://vite.dev/
License: MIT License
Copyright: 2019-present VoidZero Inc. and Vite contributors
---
Vite-plugin-static-copy: A Vite plugin for copying static files
URL: https://github.com/sapphi-red/vite-plugin-static-copy
License: MIT License
Copyright: 2021 sapphi-red
---
Vite-tsconfig-paths: A Vite plugin for resolving TypeScript paths
URL: https://github.com/aleclarson/vite-tsconfig-paths
License: MIT License
Copyright: Alec Larson
---
Web Component Analyzer: A tool for analyzing web components
URL: https://github.com/runem/web-component-analyzer
License: MIT License
Copyright: 2019 Rune Mehlsen

View File

@@ -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);

View File

@@ -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.
}
}

View File

@@ -377,7 +377,7 @@
<key alias="invalidEmpty">Value cannot be empty</key>
<key alias="invalidPattern">Value is invalid, it does not match the correct pattern</key>
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
<key alias="invalidMemberGroupName">Invalid member group name</key>

View File

@@ -394,7 +394,7 @@
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
<key alias="invalidMediaType">The chosen media type is invalid.</key>

View File

@@ -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}";
}

View File

@@ -1,5 +1,3 @@
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
@@ -67,19 +65,14 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ
{
// Get all ids in the path for the content item and ensure they all
// parse to ints that are not -1.
var ids = contentPath.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
.Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1)
.Where(x => x != -1)
.ToList();
// start with the deepest id
ids.Reverse();
// Start with the deepest id.
IEnumerable<int> ids = contentPath.GetIdsFromPathReversed().Where(x => x != -1);
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
// This will retrieve from cache!
var entries = _publicAccessRepository.GetMany().ToList();
foreach (var id in CollectionsMarshal.AsSpan(ids))
foreach (var id in ids)
{
PublicAccessEntry? found = entries.Find(x => x.ProtectedNodeId == id);
if (found != null)
@@ -286,7 +279,7 @@ internal sealed class PublicAccessService : RepositoryService, IPublicAccessServ
return Attempt.FailWithStatus(PublicAccessOperationStatus.NoAllowedEntities, result);
}
if(entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
if (entry.MemberUserNames.Any() && entry.MemberGroupNames.Any())
{
return Attempt.FailWithStatus(PublicAccessOperationStatus.AmbiguousRule, result);
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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)

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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();

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": {

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"

View File

@@ -2057,7 +2057,7 @@ export default {
duplicateUsername: "Username '%0%' is already taken",
customValidation: 'Custom validation',
entriesShort: 'Minimum %0% entries, requires <strong>%1%</strong> more.',
entriesExceed: 'Maximum %0% entries, <strong>%1%</strong> too many.',
entriesExceed: 'Maximum %0% entries, you have entered <strong>%1%</strong> too many.',
entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.',
},
healthcheck: {

View File

@@ -2170,7 +2170,7 @@ export default {
invalidPattern: 'Value is invalid, it does not match the correct pattern',
customValidation: 'Custom validation',
entriesShort: 'Minimum %0% entries, requires <strong>%1%</strong> more.',
entriesExceed: 'Maximum %0% entries, <strong>%1%</strong> too many.',
entriesExceed: 'Maximum %0% entries, you have entered <strong>%1%</strong> too many.',
entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.',
invalidMemberGroupName: 'Invalid member group name',
invalidUserGroupName: 'Invalid user group name',

View File

@@ -1,9 +1,9 @@
/*
* This Source Code has been derived from Jameson Little's base64-js.
* This Source Code has been derived from base64-js.
* https://github.com/beatgammit/base64-js
* SPDX-License-Identifier: MIT
* Copyright © 2014 Jameson Little.
* Modifications are licensed under the MIT License.
* Copyright © 2014 Jameson Little
* Copyright © 2024 Umbraco A/S
* Licensed under the MIT License.
*/
const lookup: string[] = [];

View File

@@ -1,16 +0,0 @@
Lucide License
ISC License <https://lucide.dev/license>
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
Simple Icons
CC0 1.0 Universal license <https://creativecommons.org/publicdomain/zero/1.0/>
The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.

View File

@@ -1,6 +1,9 @@
This Source Code has been derived from router-slot.
The MIT License (MIT)
Copyright © 2018 Andreas Mehlsen andmehlsen@gmail.com
Copyright © 2018 Andreas Mehlsen <andmehlsen@gmail.com>
Copyright © 2023 Umbraco A/S
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@@ -1,8 +1,10 @@
/* This Source Code has been derived from Lee Kelleher's Contentment.
/*
* This Source Code has been derived from Contentment.
* https://github.com/leekelleher/umbraco-contentment/blob/develop/src/Umbraco.Community.Contentment/DataEditors/Bytes/bytes.js
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2019 Lee Kelleher.
* Modifications are licensed under the MIT License.
* Copyright © 2016-2023 Lee Kelleher
* Copyright © 2024 Umbraco A/S
* Originally licensed under the Mozilla Public License, v. 2.0
* Relicensed under the MIT License with permission from the copyright holder.
*/
export interface IFormatBytesOptions {

View File

@@ -461,6 +461,7 @@ export function getMimeTypeFromExtension(extension: string): string | null {
'.onetoc2': 'application/onenote',
'.opf': 'application/oebps-package+xml',
'.oprc': 'application/vnd.palm',
'.opus': 'audio/ogg',
'.org': 'application/vnd.lotus-organizer',
'.osf': 'application/vnd.yamaha.openscoreformat',
'.osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
@@ -744,6 +745,8 @@ export function getMimeTypeFromExtension(extension: string): string | null {
'.wbxml': 'application/vnd.wap.wbxml',
'.wcm': 'application/vnd.ms-works',
'.wdb': 'application/vnd.ms-works',
'.weba': 'audio/webm',
'.webm': 'video/webm',
'.webp': 'image/webp',
'.wiz': 'application/msword',
'.wks': 'application/vnd.ms-works',

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": {

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": {

View File

@@ -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);
});

View File

@@ -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}) => {

View File

@@ -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);
});

View File

@@ -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}) => {

View File

@@ -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]]);
});

View File

@@ -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(`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);
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
});
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButton();
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);
// 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([]);
});
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateButton();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickSaveButton();
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);
// 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([]);
});
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveAndPublishButton();
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);
// 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([]);
});
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickSaveAndPublishButton();
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);
// 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([]);
});
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.chooseDropdownOption(selectedOptions);
await umbracoUi.content.clickSaveAndPublishButton();
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);
});
// 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]]);
});

View File

@@ -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([]);
});

View File

@@ -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]);
});

View File

@@ -82,4 +82,3 @@ test('can remove a tag in the content', async ({umbracoApi, umbracoUi}) => {
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values).toEqual([]);
});

View File

@@ -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}) => {

View File

@@ -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; }
}
}