Merge branch 'main' into v17/dev

This commit is contained in:
Laura Neto
2025-09-09 10:17:47 +02:00
16 changed files with 456 additions and 14 deletions

View File

@@ -51,6 +51,7 @@ public static class UmbracoBuilderExtensions
});
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
@@ -16,12 +16,12 @@ internal sealed class ContextualizeFromAcceptHeadersAttribute : TypeFilterAttrib
private sealed class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
{
private readonly IRequestCultureService _requestCultureService;
private readonly IRequestSegmmentService _requestSegmentService;
private readonly IRequestSegmentService _requestSegmentService;
private readonly IVariationContextAccessor _variationContextAccessor;
public LocalizeFromAcceptLanguageHeaderAttributeFilter(
IRequestCultureService requestCultureService,
IRequestSegmmentService requestSegmentService,
IRequestSegmentService requestSegmentService,
IVariationContextAccessor variationContextAccessor)
{
_requestCultureService = requestCultureService;

View File

@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Api.Delivery.Services;
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmmentService
internal sealed class RequestSegmentService : RequestHeaderHandler, IRequestSegmentService, IRequestSegmmentService
{
public RequestSegmentService(IHttpContextAccessor httpContextAccessor)
: base(httpContextAccessor)

View File

@@ -0,0 +1,85 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Segment;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Document;
[Obsolete("This controller is temporary and will be removed in a future release (planned for v20). A more permanent solution will follow.")]
[ApiVersion("1.0")]
public class AvailableSegmentsController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly ISegmentService _segmentService;
private readonly IUmbracoMapper _umbracoMapper;
public AvailableSegmentsController(
IAuthorizationService authorizationService,
ISegmentService segmentService,
IUmbracoMapper umbracoMapper)
{
_authorizationService = authorizationService;
_segmentService = segmentService;
_umbracoMapper = umbracoMapper;
}
[HttpGet("{id:guid}/available-segment-options")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<SegmentResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> GetAvailableSegmentOptions(
Guid id,
CancellationToken cancellationToken,
int skip = 0,
int take = 100)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionBrowse.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);
if (!authorizationResult.Succeeded)
{
return Forbidden();
}
Attempt<PagedModel<Core.Models.Segment>?, SegmentOperationStatus> pagedAttempt =
await _segmentService.GetPagedSegmentsForDocumentAsync(id, skip, take);
if (pagedAttempt.Success is false)
{
return MapFailure(pagedAttempt.Status);
}
var viewModel = new PagedViewModel<SegmentResponseModel>
{
Items = _umbracoMapper.MapEnumerable<Core.Models.Segment, SegmentResponseModel>(pagedAttempt.Result!.Items),
Total = pagedAttempt.Result!.Total,
};
return Ok(viewModel);
}
private IActionResult MapFailure(SegmentOperationStatus status)
=> OperationStatusResult(
status,
problemDetailsBuilder => status switch
{
_ => StatusCode(
StatusCodes.Status500InternalServerError,
problemDetailsBuilder
.WithTitle("Unknown segment operation status.")
.Build()),
});
}

View File

@@ -5,12 +5,15 @@ namespace Umbraco.Cms.Api.Management.Mapping.Segment;
public class SegmentMapDefinition : IMapDefinition
{
public void DefineMaps(IUmbracoMapper mapper) => mapper.Define<Core.Models.Segment, SegmentResponseModel>((_, _) => new SegmentResponseModel { Name = string.Empty, Alias = string.Empty }, Map);
public void DefineMaps(IUmbracoMapper mapper) => mapper.Define<Core.Models.Segment, SegmentResponseModel>(
(_, _) => new SegmentResponseModel { Name = string.Empty, Alias = string.Empty, Cultures = null },
Map);
// Umbraco.Code.MapAll
private static void Map(Core.Models.Segment source, SegmentResponseModel target, MapperContext context)
{
target.Name = source.Name;
target.Alias = source.Alias;
target.Cultures = source.Cultures;
}
}

View File

@@ -1,5 +1,3 @@
using System.ComponentModel.DataAnnotations;
namespace Umbraco.Cms.Api.Management.ViewModels.Segment;
public class SegmentResponseModel
@@ -7,4 +5,7 @@ public class SegmentResponseModel
public required string Name { get; set; } = string.Empty;
public required string Alias { get; set; } = string.Empty;
[Obsolete("This property is temporary and will be removed in a future release (planned for v20). A more permanent solution will follow.")]
public IEnumerable<string>? Cultures { get; set; } = null;
}

View File

@@ -1,5 +1,14 @@
namespace Umbraco.Cms.Core.DeliveryApi;
namespace Umbraco.Cms.Core.DeliveryApi;
public interface IRequestSegmentService
{
/// <summary>
/// Gets the requested segment from the "Accept-Segment" header, if present.
/// </summary>
string? GetRequestedSegment();
}
[Obsolete("This interface is misspelled and will be removed in Umbraco 18. Please use the correct one IRequestSegmentService")]
public interface IRequestSegmmentService
{
/// <summary>

View File

@@ -5,4 +5,7 @@ public class Segment
public required string Name { get; set; }
public required string Alias { get; set; }
[Obsolete("This property is temporary and will be removed in a future release (planned for v20). A more permanent solution will follow.")]
public IEnumerable<string>? Cultures { get; set; } = null;
}

View File

@@ -5,5 +5,27 @@ namespace Umbraco.Cms.Core.Services;
public interface ISegmentService
{
Task<Attempt<PagedModel<Segment>?, SegmentOperationStatus>> GetPagedSegmentsAsync(int skip = 0, int take = 100);
/// <summary>
/// Gets a paged list of segments.
/// </summary>
/// <param name="skip">The number of items to skip.</param>
/// <param name="take">The number of items to take.</param>
/// <returns>The paged list of segments.</returns>
Task<Attempt<PagedModel<Segment>?, SegmentOperationStatus>> GetPagedSegmentsAsync(
int skip = 0,
int take = 100);
/// <summary>
/// Gets a paged list of segments for a specific document.
/// </summary>
/// <param name="id">The document unique identifier.</param>
/// <param name="skip">The number of items to skip.</param>
/// <param name="take">The number of items to take.</param>
/// <returns>The paged list of segments.</returns>
[Obsolete("This method is temporary and will be removed in a future release (planned for v20). A more permanent solution will follow.")]
Task<Attempt<PagedModel<Segment>?, SegmentOperationStatus>> GetPagedSegmentsForDocumentAsync(
Guid id,
int skip = 0,
int take = 100)
=> GetPagedSegmentsAsync(skip, take);
}

View File

@@ -1,8 +1,16 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Data.SqlTypes;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -25,8 +33,91 @@ public class DateTimePropertyEditor : DataEditor
/// <inheritdoc />
protected override IDataValueEditor CreateValueEditor()
{
IDataValueEditor editor = base.CreateValueEditor();
DateTimePropertyValueEditor editor = DataValueEditorFactory.Create<DateTimePropertyValueEditor>(Attribute!);
editor.Validators.Add(new DateTimeValidator());
return editor;
}
/// <summary>
/// Provides a value editor for the datetime property editor.
/// </summary>
internal sealed class DateTimePropertyValueEditor : DataValueEditor
{
/// <summary>
/// The key used to retrieve the date format from the data type configuration.
/// </summary>
internal const string DateTypeConfigurationFormatKey = "format";
/// <summary>
/// Initializes a new instance of the <see cref="DateTimePropertyValueEditor"/> class.
/// </summary>
public DateTimePropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
}
/// <inheritdoc/>
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
{
if (editorValue.Value is null)
{
return base.FromEditor(editorValue, currentValue);
}
if (TryGetConfiguredDateTimeFormat(editorValue, out string? format) is false)
{
return base.FromEditor(editorValue, currentValue);
}
if (IsTimeOnlyFormat(format) is false)
{
return base.FromEditor(editorValue, currentValue);
}
// We have a time-only format, so we need to ensure the date part is valid for SQL Server, so we can persist
// without error.
// If we have a date part that is less than the minimum date, we need to adjust it to be the minimum date.
Attempt<object?> dateConvertAttempt = editorValue.Value.TryConvertTo(typeof(DateTime?));
if (dateConvertAttempt.Success is false || dateConvertAttempt.Result is null)
{
return base.FromEditor(editorValue, currentValue);
}
var dateTimeValue = (DateTime)dateConvertAttempt.Result;
int yearValue = dateTimeValue.Year > SqlDateTime.MinValue.Value.Year
? dateTimeValue.Year
: SqlDateTime.MinValue.Value.Year;
return new DateTime(yearValue, dateTimeValue.Month, dateTimeValue.Day, dateTimeValue.Hour, dateTimeValue.Minute, dateTimeValue.Second);
}
private static bool TryGetConfiguredDateTimeFormat(ContentPropertyData editorValue, [NotNullWhen(true)] out string? format)
{
if (editorValue.DataTypeConfiguration is not Dictionary<string, object> dataTypeConfigurationDictionary)
{
format = null;
return false;
}
KeyValuePair<string, object> keyValuePair = dataTypeConfigurationDictionary
.FirstOrDefault(kvp => kvp.Key is "format");
format = keyValuePair.Value as string;
return string.IsNullOrWhiteSpace(format) is false;
}
private static bool IsTimeOnlyFormat(string format)
{
DateTime testDate = DateTime.UtcNow;
var testDateFormatted = testDate.ToString(format);
if (DateTime.TryParseExact(testDateFormatted, format, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out DateTime parsedDate) is false)
{
return false;
}
return parsedDate.Year == 1 && parsedDate.Month == 1 && parsedDate.Day == 1;
}
}
}

View File

@@ -98,6 +98,10 @@ export class UmbPropertyEditorUIDropdownElement
} else {
this.addFormControlElement(this.shadowRoot!.querySelector('umb-input-dropdown-list')!);
}
if (!this.mandatory && !this._multiple) {
this._options.unshift({ name: '', value: '', selected: false, invalid: false });
}
}
#onChange(event: CustomEvent & { target: UmbInputDropdownListElement }) {

View File

@@ -7,7 +7,7 @@
"name": "acceptancetest",
"hasInstallScript": true,
"dependencies": {
"@umbraco/json-models-builders": "^2.0.37",
"@umbraco/json-models-builders": "^2.0.38",
"@umbraco/playwright-testhelpers": "^16.0.42",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",

View File

@@ -21,7 +21,7 @@
"typescript": "^4.8.3"
},
"dependencies": {
"@umbraco/json-models-builders": "^2.0.37",
"@umbraco/json-models-builders": "^2.0.38",
"@umbraco/playwright-testhelpers": "^16.0.42",
"camelize": "^1.0.0",
"dotenv": "^16.3.1",

View File

@@ -172,5 +172,4 @@ test('can remove content picker in the content', async ({umbracoApi, umbracoUi})
await umbracoUi.content.isSuccessStateVisibleForSaveButton();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values).toEqual([]);
});
});

View File

@@ -0,0 +1,183 @@
import {ConstantHelper, NotificationConstantHelper, test} from '@umbraco/playwright-testhelpers';
import {expect} from "@playwright/test";
const contentName = 'TestContent';
const documentTypeName = 'TestDocumentTypeForContent';
const customDataTypeName = 'CustomMultiNodeTreePicker';
test.beforeEach(async ({umbracoApi}) => {
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.document.ensureNameNotExists(contentName);
});
test.afterEach(async ({umbracoApi}) => {
await umbracoApi.document.ensureNameNotExists(contentName);
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
await umbracoApi.dataType.ensureNameNotExists(customDataTypeName);
});
test('can create content with content picker with allowed types', async ({umbracoApi, umbracoUi}) => {
// Arrange
const allowedContentPickerDocumentTypeName = 'ContentPickerDocumentType';
const allowedContentPickerName = 'Test Content Picker';
const notAllowedContentPickerName = 'Not Allowed Test Content Picker';
const allowedContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(allowedContentPickerDocumentTypeName);
const allowedContentPickerId = await umbracoApi.document.createDefaultDocument(allowedContentPickerName, allowedContentPickerDocumentTypeId);
// Create a custom content picker with predefined allowed types
const customDataTypeId = await umbracoApi.dataType.createMultiNodeTreePickerDataTypeWithAllowedTypes(customDataTypeName, allowedContentPickerDocumentTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(notAllowedContentPickerName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.clickActionsMenuAtRoot();
await umbracoUi.content.clickCreateActionMenuOption();
await umbracoUi.content.chooseDocumentType(documentTypeName);
await umbracoUi.content.enterContentName(contentName);
await umbracoUi.content.clickChooseButton();
await umbracoUi.content.isModalMenuItemWithNameDisabled(notAllowedContentPickerName);
await umbracoUi.content.selectLinkByName(allowedContentPickerName);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.clickSaveAndPublishButton();
// Assert
await umbracoUi.content.doesSuccessNotificationHaveText(NotificationConstantHelper.success.published);
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value[0]['unique']).toEqual(allowedContentPickerId);
expect(contentData.values[0].value[0]['type']).toEqual('document');
// Clean
await umbracoApi.document.ensureNameNotExists(allowedContentPickerName);
await umbracoApi.document.ensureNameNotExists(notAllowedContentPickerName);
});
test('can search and see only allowed content types', async ({umbracoApi, umbracoUi}) => {
// Arrange
const allowedContentPickerDocumentTypeName = 'ContentPickerDocumentType';
const allowedContentPickerName = 'Test Content Picker';
const notAllowedContentPickerName = 'Not Allowed Test Content Picker';
const allowedContentPickerDocumentTypeId = await umbracoApi.documentType.createDefaultDocumentTypeWithAllowAsRoot(allowedContentPickerDocumentTypeName);
const allowedContentPickerId = await umbracoApi.document.createDefaultDocument(allowedContentPickerName, allowedContentPickerDocumentTypeId);
// Create a content with custom content picker with predefined allowed types
const customDataTypeId = await umbracoApi.dataType.createMultiNodeTreePickerDataTypeWithAllowedTypes(customDataTypeName, allowedContentPickerDocumentTypeId);
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoApi.document.createDefaultDocument(notAllowedContentPickerName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickChooseButton();
// Assert
await umbracoUi.content.isModalMenuItemWithNameVisible(allowedContentPickerName);
await umbracoUi.content.isModalMenuItemWithNameDisabled(notAllowedContentPickerName);
await umbracoUi.content.enterSearchKeywordInTreePickerModal('Picker');
await umbracoUi.content.isModalMenuItemWithNameVisible(notAllowedContentPickerName, false);
await umbracoUi.content.clickEntityItemByName(allowedContentPickerName);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessStateIconVisible();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value[0]['unique']).toEqual(allowedContentPickerId);
expect(contentData.values[0].value[0]['type']).toEqual('document');
// Clean
await umbracoApi.document.ensureNameNotExists(allowedContentPickerName);
await umbracoApi.document.ensureNameNotExists(notAllowedContentPickerName);
});
test('can search and see only allowed media types', async ({umbracoApi, umbracoUi}) => {
// Arrange
const allowedMediaPickerName = 'Test Image';
const notAllowedMediaPickerName = 'Test Article';
const allowedMediaPickerId = await umbracoApi.media.createDefaultMediaWithImage(allowedMediaPickerName);
await umbracoApi.media.createDefaultMediaWithArticle(notAllowedMediaPickerName);
const imageMediaTypeData = await umbracoApi.mediaType.getByName('Image');
// Create a content with custom tree picker with predefined allowed media types
const customDataTypeId = await umbracoApi.dataType.createMultiNodeTreePickerDataTypeWithAllowedTypes(customDataTypeName, imageMediaTypeData.id, 'media');
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickChooseButton();
// Assert
await umbracoUi.content.isMediaCardItemWithNameDisabled(notAllowedMediaPickerName);
await umbracoUi.content.isMediaCardItemWithNameVisible(allowedMediaPickerName);
await umbracoUi.content.enterSearchKeywordInMediaPickerModal('Test');
await umbracoUi.content.isMediaCardItemWithNameVisible(notAllowedMediaPickerName, false);
await umbracoUi.content.clickMediaWithName(allowedMediaPickerName);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.clickSaveButton();
// Assert
await umbracoUi.content.isSuccessStateIconVisible();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value[0]['unique']).toEqual(allowedMediaPickerId);
expect(contentData.values[0].value[0]['type']).toEqual('media');
// Clean
await umbracoApi.media.ensureNameNotExists(allowedMediaPickerName);
await umbracoApi.media.ensureNameNotExists(notAllowedMediaPickerName);
});
test('can search and see only allowed member types', async ({umbracoApi, umbracoUi}) => {
// Arrange
// Not allowed member type
const notAllowedMemberTypeName = 'Not Allowed Member Type';
const notAllowedMemberTypeId = await umbracoApi.memberType.createDefaultMemberType(notAllowedMemberTypeName);
// Allowed member type
const allowedMemberTypeData = await umbracoApi.memberType.getByName('Member');
// Allowed member
const allowedTestMember = {
name : 'Allowed Test Member',
username : 'allowedTestMember',
email : 'allowedTestMember@acceptance.test',
password : '0123456789',
};
const allowedTestMemberId = await umbracoApi.member.createDefaultMember(allowedTestMember.name, allowedMemberTypeData.id, allowedTestMember.email, allowedTestMember.username, allowedTestMember.password);
// Not allowed member
const notAllowedTestMember = {
name : 'Not Allowed Test Member',
username : 'notAllowedTestMember',
email : 'notAllowedTestMember@acceptance.test',
password : '0123456789',
};
await umbracoApi.member.createDefaultMember(notAllowedTestMember.name, notAllowedMemberTypeId, notAllowedTestMember.email, notAllowedTestMember.username, notAllowedTestMember.password);
// Create a content with custom tree picker with predefined allowed member types
const customDataTypeId = await umbracoApi.dataType.createMultiNodeTreePickerDataTypeWithAllowedTypes(customDataTypeName, allowedMemberTypeData.id, 'member');
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId);
await umbracoApi.document.createDefaultDocument(contentName, documentTypeId);
await umbracoUi.goToBackOffice();
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
// Act
await umbracoUi.content.goToContentWithName(contentName);
await umbracoUi.content.clickChooseButton();
// Assert
await umbracoUi.content.isModalMenuItemWithNameVisible(allowedTestMember.name);
await umbracoUi.content.isModalMenuItemWithNameDisabled(notAllowedTestMember.name);
await umbracoUi.content.enterSearchKeywordInMemberPickerModal('Allowed Test Member');
await umbracoUi.content.isModalMenuItemWithNameVisible(notAllowedTestMember.name, false);
await umbracoUi.content.clickEntityItemByName(allowedTestMember.name);
await umbracoUi.content.clickChooseModalButton();
await umbracoUi.content.clickSaveButton();
await umbracoUi.content.isSuccessStateIconVisible();
const contentData = await umbracoApi.document.getByName(contentName);
expect(contentData.values[0].value[0]['unique']).toEqual(allowedTestMemberId);
expect(contentData.values[0].value[0]['type']).toEqual('member');
// Clean
await umbracoApi.member.ensureNameNotExists(allowedTestMember.name);
await umbracoApi.member.ensureNameNotExists(notAllowedTestMember.name);
await umbracoApi.memberType.ensureNameNotExists(notAllowedMemberTypeName);
});

View File

@@ -0,0 +1,41 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PropertyEditors;
public class DateTimePropertyEditorTests
{
// Various time formats with years below the minimum, so we expect to increase the date to the minimum supported by SQL Server.
[TestCase("01/01/0001 10:00", "01/01/1753 10:00", "hh:mm")]
[TestCase("01/01/0001 10:00", "01/01/1753 10:00", "HH:mm")]
[TestCase("01/01/0001 10:00", "01/01/1753 10:00", "hh mm")]
[TestCase("10/10/1000 10:00", "10/10/1753 10:00", "hh:mm:ss")]
[TestCase("10/10/1000 10:00", "10/10/1753 10:00", "hh-mm-ss")]
// Time format with year above the minimum, so we expect to not convert.
[TestCase("01/01/2000 10:00", "01/01/2000 10:00", "HH:mm")]
// Date formats, so we don't convert even if the year is below the minimum.
[TestCase("01/01/0001 10:00", "01/01/0001 10:00", "dd-MM-yyyy hh:mm")]
[TestCase("01/01/0001 10:00", "01/01/0001 10:00", "dd-MM-yyyy")]
[TestCase("01/01/0001 10:00", "01/01/0001 10:00", "yyyy-MM-d")]
public void Time_Only_Format_Ensures_DateTime_Can_Be_Persisted(DateTime actualDateTime, DateTime expectedDateTime, string format)
{
var dateTimePropertyEditor = new DateTimePropertyEditor.DateTimePropertyValueEditor(
Mock.Of<IShortStringHelper>(),
Mock.Of<IJsonSerializer>(),
Mock.Of<IIOHelper>(),
new DataEditorAttribute("Alias") { ValueType = ValueTypes.DateTime.ToString() });
Dictionary<string, object> dictionary = new Dictionary<string, object> { { DateTimePropertyEditor.DateTimePropertyValueEditor.DateTypeConfigurationFormatKey, format } };
ContentPropertyData propertyData = new ContentPropertyData(actualDateTime, dictionary);
var value = (DateTime)dateTimePropertyEditor.FromEditor(propertyData, null);
Assert.AreEqual(expectedDateTime, value);
}
}