From 2eebd0558cd9e8bd0e1fbdecadc2a2315fdbe062 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 21 Feb 2023 13:40:41 +0100 Subject: [PATCH] Document and document type read API (#13853) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Basic structure for document and document type read API * Handle unpublished, non-variant content * Expose content type key on ContentTypeSort * Add the remaining properties to document type (minus list view info, still pending) * Obsolete more ILocalizationService usage * Add URLs and template data to document view model * Clean up + add proprety type appearance * update submodule commit * front-end commit * latest front-end commit * latest commit * latest front-end commit * Rename content property to content value in view model layer * Add contextual JSON serialization as default JSON serializer * Add FIXME to content type sort + rearrange constructor parameters * Fix broken remark tag * Whitelist breakage for ContentTypeSort * Add variance info to property type output * Update src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs Co-authored-by: Bjarke Berg * Update src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs Co-authored-by: Bjarke Berg * Update src/Umbraco.Cms.Api.Management/Factories/ContentUrlFactory.cs Co-authored-by: Bjarke Berg * Add a few FIXME comments about async entity retrieval --------- Co-authored-by: Niels Lyngsø Co-authored-by: Bjarke Berg --- .../Document/ByKeyDocumentController.cs | 37 ++ .../Document/DocumentControllerBase.cs | 13 + .../ByKeyDocumentTypeController.cs | 37 ++ .../DocumentType/DocumentControllerBase.cs | 13 + .../DocumentBuilderExtensions.cs | 20 + .../DocumentTypeBuilderExtensions.cs | 15 + .../Factories/ContentUrlFactory.cs | 66 +++ .../Factories/DocumentViewModelFactory.cs | 36 ++ .../Factories/IContentUrlFactory.cs | 9 + .../Factories/IDocumentViewModelFactory.cs | 9 + .../ManagementApiComposer.cs | 2 + .../Mapping/Content/ContentMapDefinition.cs | 71 +++ .../ContentType/ContentTypeMapDefinition.cs | 78 +++ .../Mapping/Document/DocumentMapDefinition.cs | 57 ++ .../DocumentType/DocumentTypeMapDefinition.cs | 62 +++ src/Umbraco.Cms.Api.Management/OpenApi.json | 514 ++++++++++++++++++ .../ViewModels/Content/ContentState.cs | 27 + .../ViewModels/Content/ContentUrlInfo.cs | 8 + .../Content/ContentViewModelBase.cs | 14 + .../ViewModels/Content/ValueViewModelBase.cs | 12 + .../Content/VariantViewModelBase.cs | 14 + .../ContentType/ContentTypeCleanup.cs | 10 + .../ContentType/ContentTypeComposition.cs | 8 + .../ContentType/ContentTypeCompositionType.cs | 7 + .../ViewModels/ContentType/ContentTypeSort.cs | 8 + .../ContentType/ContentTypeViewModelBase.cs | 34 ++ .../ContentType/PropertyTypeAppearance.cs | 6 + .../PropertyTypeContainerViewModelBase.cs | 15 + .../ContentType/PropertyTypeValidation.cs | 12 + .../ContentType/PropertyTypeViewModelBase.cs | 24 + .../Document/DocumentValueViewModel.cs | 7 + .../Document/DocumentVariantViewModel.cs | 10 + .../ViewModels/Document/DocumentViewModel.cs | 10 + ...umentTypePropertyTypeContainerViewModel.cs | 7 + .../DocumentTypePropertyTypeViewModel.cs | 7 + .../DocumentType/DocumentTypeViewModel.cs | 10 + .../CompatibilitySuppressions.xml | 7 + src/Umbraco.Core/Models/ContentTypeSort.cs | 10 +- .../Routing/UrlProviderExtensions.cs | 43 +- .../UmbracoBuilder.CoreServices.cs | 2 +- .../Models/Mapping/EntityMapDefinition.cs | 1 + .../Packaging/PackageDataInstallation.cs | 3 +- .../Implement/ContentTypeCommonRepository.cs | 8 +- .../Serialization/ContextualJsonSerializer.cs | 49 ++ ...emTextConfigurationEditorJsonSerializer.cs | 77 +-- .../Serialization/SystemTextJsonSerializer.cs | 24 + tests/Umbraco.TestData/LoadTestController.cs | 2 +- .../Builders/ContentTypeSortBuilder.cs | 3 +- .../Repositories/ContentTypeRepositoryTest.cs | 4 +- .../Services/ContentServicePerformanceTest.cs | 12 +- .../Services/ContentServiceTagsTests.cs | 2 +- .../Services/ContentServiceTests.cs | 4 +- .../UrlAndDomains/DomainAndUrlsTests.cs | 2 +- .../Routing/GetContentUrlsTests.cs | 11 +- 54 files changed, 1444 insertions(+), 109 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentControllerBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/ContentUrlFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/DocumentViewModelFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IContentUrlFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Factories/IDocumentViewModelFactory.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentState.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentUrlInfo.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentViewModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/ValueViewModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/VariantViewModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCleanup.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeComposition.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCompositionType.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeSort.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeViewModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeAppearance.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeContainerViewModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeValidation.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeViewModelBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeContainerViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeViewModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeViewModel.cs create mode 100644 src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs create mode 100644 src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs new file mode 100644 index 0000000000..1644eac84b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ByKeyDocumentController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +public class ByKeyDocumentController : DocumentControllerBase +{ + private readonly IContentService _contentService; + private readonly IDocumentViewModelFactory _documentViewModelFactory; + + public ByKeyDocumentController(IContentService contentService, IDocumentViewModelFactory documentViewModelFactory) + { + _contentService = contentService; + _documentViewModelFactory = documentViewModelFactory; + } + + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(DocumentViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ByKey(Guid key) + { + // FIXME: create and use an async get method here. + IContent? content = _contentService.GetById(key); + if (content == null) + { + return NotFound(); + } + + DocumentViewModel model = await _documentViewModelFactory.CreateViewModelAsync(content); + return Ok(model); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs new file mode 100644 index 0000000000..fe8bd8a1c0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Api.Management.Controllers.Document; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute(Constants.UdiEntityType.Document)] +[ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))] +public abstract class DocumentControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs new file mode 100644 index 0000000000..8fc1d43293 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/ByKeyDocumentTypeController.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; + +public class ByKeyDocumentTypeController : DocumentTypeControllerBase +{ + private readonly IContentTypeService _contentTypeService; + private readonly IUmbracoMapper _umbracoMapper; + + public ByKeyDocumentTypeController(IContentTypeService contentTypeService, IUmbracoMapper umbracoMapper) + { + _contentTypeService = contentTypeService; + _umbracoMapper = umbracoMapper; + } + + [HttpGet("{key:guid}")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(DocumentTypeViewModel), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task ByKey(Guid key) + { + // FIXME: create and use an async get method here. + IContentType? contentType = _contentTypeService.Get(key); + if (contentType == null) + { + return NotFound(); + } + + DocumentTypeViewModel model = _umbracoMapper.Map(contentType)!; + return await Task.FromResult(Ok(model)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentControllerBase.cs new file mode 100644 index 0000000000..d5279cfde4 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DocumentControllerBase.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DocumentType)] +[ApiExplorerSettings(GroupName = "Document Type")] +public abstract class DocumentTypeControllerBase : ManagementApiControllerBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentBuilderExtensions.cs new file mode 100644 index 0000000000..b3c6130614 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentBuilderExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.Mapping.Document; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class DocumentBuilderExtensions +{ + internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs new file mode 100644 index 0000000000..f0bbb227c1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentTypeBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Api.Management.Mapping.DocumentType; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +internal static class DocumentTypeBuilderExtensions +{ + internal static IUmbracoBuilder AddDocumentTypes(this IUmbracoBuilder builder) + { + builder.WithCollectionBuilder().Add(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/ContentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ContentUrlFactory.cs new file mode 100644 index 0000000000..9327038b76 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/ContentUrlFactory.cs @@ -0,0 +1,66 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class ContentUrlFactory : IContentUrlFactory +{ + private readonly IPublishedRouter _publishedRouter; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly ILanguageService _languageService; + private readonly ILocalizedTextService _localizedTextService; + private readonly IContentService _contentService; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly ILoggerFactory _loggerFactory; + private readonly UriUtility _uriUtility; + private readonly IPublishedUrlProvider _publishedUrlProvider; + + public ContentUrlFactory( + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILanguageService languageService, + ILocalizedTextService localizedTextService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILoggerFactory loggerFactory, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider) + { + _publishedRouter = publishedRouter; + _umbracoContextAccessor = umbracoContextAccessor; + _languageService = languageService; + _localizedTextService = localizedTextService; + _contentService = contentService; + _variationContextAccessor = variationContextAccessor; + _loggerFactory = loggerFactory; + _uriUtility = uriUtility; + _publishedUrlProvider = publishedUrlProvider; + } + + public async Task> GetUrlsAsync(IContent content) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + UrlInfo[] urlInfos = (await content.GetContentUrlsAsync( + _publishedRouter, + umbracoContext, + _languageService, + _localizedTextService, + _contentService, + _variationContextAccessor, + _loggerFactory.CreateLogger(), + _uriUtility, + _publishedUrlProvider)).ToArray(); + + return urlInfos + .Where(urlInfo => urlInfo.IsUrl) + .Select(urlInfo => new ContentUrlInfo { Culture = urlInfo.Culture, Url = urlInfo.Text }) + .ToArray(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentViewModelFactory.cs new file mode 100644 index 0000000000..93f2e553a7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentViewModelFactory.cs @@ -0,0 +1,36 @@ +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class DocumentViewModelFactory : IDocumentViewModelFactory +{ + private readonly IUmbracoMapper _umbracoMapper; + private readonly IContentUrlFactory _contentUrlFactory; + private readonly IFileService _fileService; + + public DocumentViewModelFactory( + IUmbracoMapper umbracoMapper, + IContentUrlFactory contentUrlFactory, + IFileService fileService) + { + _umbracoMapper = umbracoMapper; + _contentUrlFactory = contentUrlFactory; + _fileService = fileService; + } + + public async Task CreateViewModelAsync(IContent content) + { + DocumentViewModel viewModel = _umbracoMapper.Map(content)!; + + viewModel.Urls = await _contentUrlFactory.GetUrlsAsync(content); + + viewModel.TemplateKey = content.TemplateId.HasValue + ? _fileService.GetTemplate(content.TemplateId.Value)?.Key + : null; + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IContentUrlFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IContentUrlFactory.cs new file mode 100644 index 0000000000..c48cb67052 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IContentUrlFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IContentUrlFactory +{ + Task> GetUrlsAsync(IContent content); +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentViewModelFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentViewModelFactory.cs new file mode 100644 index 0000000000..46da5746b6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentViewModelFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IDocumentViewModelFactory +{ + Task CreateViewModelAsync(IContent content); +} diff --git a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs index 9b539cc742..1bb5f1ff4c 100644 --- a/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.Api.Management/ManagementApiComposer.cs @@ -25,6 +25,8 @@ public class ManagementApiComposer : IComposer .AddUpgrader() .AddSearchManagement() .AddTrees() + .AddDocuments() + .AddDocumentTypes() .AddLanguages() .AddDictionary() .AddFileUpload() diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs new file mode 100644 index 0000000000..5d381d74da --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs @@ -0,0 +1,71 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Mapping.Content; + +public abstract class ContentMapDefinition + where TContent : IContentBase + where TValueViewModel : ValueViewModelBase, new() + where TVariantViewModel : VariantViewModelBase, new() +{ + private readonly PropertyEditorCollection _propertyEditorCollection; + + protected ContentMapDefinition(PropertyEditorCollection propertyEditorCollection) => _propertyEditorCollection = propertyEditorCollection; + + protected delegate void ValueViewModelMapping(IDataEditor propertyEditor, TValueViewModel variantViewModel); + + protected delegate void VariantViewModelMapping(string? culture, string? segment, TVariantViewModel variantViewModel); + + protected IEnumerable MapValueViewModels(TContent source, ValueViewModelMapping? additionalPropertyMapping = null) => + source + .Properties + .SelectMany(property => property + .Values + .Select(propertyValue => + { + IDataEditor? propertyEditor = _propertyEditorCollection[property.PropertyType.PropertyEditorAlias]; + if (propertyEditor == null) + { + return null; + } + + var variantViewModel = new TValueViewModel + { + Culture = propertyValue.Culture, + Segment = propertyValue.Segment, + Alias = property.Alias, + Value = propertyEditor.GetValueEditor().ToEditor(property, propertyValue.Culture, propertyValue.Segment) + }; + additionalPropertyMapping?.Invoke(propertyEditor, variantViewModel); + return variantViewModel; + })) + .WhereNotNull() + .ToArray(); + + protected IEnumerable MapVariantViewModels(TContent source, VariantViewModelMapping? additionalVariantMapping = null) + { + IPropertyValue[] propertyValues = source.Properties.SelectMany(propertyCollection => propertyCollection.Values).ToArray(); + var cultures = source.AvailableCultures.DefaultIfEmpty(null).ToArray(); + var segments = propertyValues.Select(property => property.Segment).Distinct().DefaultIfEmpty(null).ToArray(); + + return cultures + .SelectMany(culture => segments.Select(segment => + { + var variantViewModel = new TVariantViewModel + { + Culture = culture, + Segment = segment, + Name = source.GetCultureName(culture) ?? string.Empty, + CreateDate = source.CreateDate, // apparently there is no culture specific creation date + UpdateDate = culture == null + ? source.UpdateDate + : source.GetUpdateDate(culture) ?? source.UpdateDate, + }; + additionalVariantMapping?.Invoke(culture, segment, variantViewModel); + return variantViewModel; + })) + .ToArray(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs new file mode 100644 index 0000000000..97dd0a8666 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs @@ -0,0 +1,78 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Mapping.ContentType; + +public abstract class ContentTypeMapDefinition + where TContentType : IContentTypeBase + where TPropertyTypeViewModel : PropertyTypeViewModelBase, new() + where TPropertyTypeContainerViewModel : PropertyTypeContainerViewModelBase, new() +{ + protected IEnumerable MapPropertyTypes(TContentType source) + { + // create a mapping table between properties and their associated groups + var groupKeysByPropertyKeys = source + .PropertyGroups + .SelectMany(propertyGroup => (propertyGroup.PropertyTypes?.ToArray() ?? Array.Empty()) + .Select(propertyType => new { GroupKey = propertyGroup.Key, PropertyTypeKey = propertyType.Key })) + .ToDictionary(map => map.PropertyTypeKey, map => map.GroupKey); + + return source.PropertyTypes.Select(propertyType => + new TPropertyTypeViewModel + { + Key = propertyType.Key, + ContainerKey = groupKeysByPropertyKeys.ContainsKey(propertyType.Key) + ? groupKeysByPropertyKeys[propertyType.Key] + : null, + Name = propertyType.Name, + Alias = propertyType.Alias, + Description = propertyType.Description, + DataTypeKey = propertyType.DataTypeKey, + VariesByCulture = propertyType.VariesByCulture(), + VariesBySegment = propertyType.VariesBySegment(), + Validation = new PropertyTypeValidation + { + Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.MandatoryMessage, + RegEx = propertyType.ValidationRegExp, + RegExMessage = propertyType.ValidationRegExpMessage + }, + Appearance = new PropertyTypeAppearance + { + LabelOnTop = propertyType.LabelOnTop + } + }) + .ToArray(); + } + + protected IEnumerable MapPropertyTypeContainers(TContentType source) + { + // create a mapping table between property group aliases and keys + var groupKeysByGroupAliases = source + .PropertyGroups + .ToDictionary(propertyGroup => propertyGroup.Alias, propertyGroup => propertyGroup.Key); + + Guid? ParentGroupKey(PropertyGroup group) + { + var path = group.Alias.Split(Constants.CharArrays.ForwardSlash); + return path.Length == 1 || groupKeysByGroupAliases.TryGetValue(path.First(), out Guid parentGroupKey) == false + ? null + : parentGroupKey; + } + + return source + .PropertyGroups + .Select(propertyGroup => + new TPropertyTypeContainerViewModel + { + Key = propertyGroup.Key, + ParentKey = ParentGroupKey(propertyGroup), + Type = propertyGroup.Type.ToString(), + SortOrder = propertyGroup.SortOrder, + Name = propertyGroup.Name, + }) + .ToArray(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs new file mode 100644 index 0000000000..4787688f21 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -0,0 +1,57 @@ +using Umbraco.Cms.Api.Management.Mapping.Content; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Api.Management.Mapping.Document; + +public class DocumentMapDefinition : ContentMapDefinition, IMapDefinition +{ + public DocumentMapDefinition(PropertyEditorCollection propertyEditorCollection) + : base(propertyEditorCollection) + { + } + + public void DefineMaps(IUmbracoMapper mapper) + => mapper.Define((_, _) => new DocumentViewModel(), Map); + + // Umbraco.Code.MapAll -Urls -TemplateKey + private void Map(IContent source, DocumentViewModel target, MapperContext context) + { + target.Key = source.Key; + target.ContentTypeKey = source.ContentType.Key; + target.Values = MapValueViewModels(source); + target.Variants = MapVariantViewModels( + source, + (culture, _, documentVariantViewModel) => + { + documentVariantViewModel.State = GetSavedState(source, culture); + documentVariantViewModel.PublishDate = culture == null + ? source.PublishDate + : source.GetPublishDate(culture); + }); + } + + private ContentState GetSavedState(IContent content, string? culture) + { + if (content.Id <= 0 || (culture != null && content.IsCultureAvailable(culture) == false)) + { + return ContentState.NotCreated; + } + + var isDraft = content.PublishedState == PublishedState.Unpublished || + (culture != null && content.IsCulturePublished(culture) == false); + if (isDraft) + { + return ContentState.Draft; + } + + var isEdited = culture != null + ? content.IsCultureEdited(culture) + : content.Edited; + + return isEdited ? ContentState.PublishedPendingChanges : ContentState.Published; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs new file mode 100644 index 0000000000..a5c5f63c53 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs @@ -0,0 +1,62 @@ +using Umbraco.Cms.Api.Management.Mapping.ContentType; +using Umbraco.Cms.Api.Management.ViewModels.ContentType; +using Umbraco.Cms.Api.Management.ViewModels.DocumentType; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; +using ContentTypeSort = Umbraco.Cms.Api.Management.ViewModels.ContentType.ContentTypeSort; + +namespace Umbraco.Cms.Api.Management.Mapping.DocumentType; + +public class DocumentTypeMapDefinition : ContentTypeMapDefinition, IMapDefinition +{ + public void DefineMaps(IUmbracoMapper mapper) + => mapper.Define((_, _) => new DocumentTypeViewModel(), Map); + + // Umbraco.Code.MapAll + private void Map(IContentType source, DocumentTypeViewModel target, MapperContext context) + { + target.Key = source.Key; + target.Alias = source.Alias; + target.Name = source.Name ?? string.Empty; + target.Description = source.Description; + target.Icon = source.Icon ?? string.Empty; + target.AllowedAsRoot = source.AllowedAsRoot; + target.VariesByCulture = source.VariesByCulture(); + target.VariesBySegment = source.VariesBySegment(); + target.IsElement = source.IsElement; + target.Containers = MapPropertyTypeContainers(source); + target.Properties = MapPropertyTypes(source); + + if (source.AllowedContentTypes != null) + { + target.AllowedContentTypes = source.AllowedContentTypes.Select(contentTypeSort + => new ContentTypeSort { Key = contentTypeSort.Key, SortOrder = contentTypeSort.SortOrder }); + } + + if (source.AllowedTemplates != null) + { + target.AllowedTemplateKeys = source.AllowedTemplates.Select(template => template.Key); + } + + target.DefaultTemplateKey = source.DefaultTemplate?.Key; + + if (source.HistoryCleanup != null) + { + target.Cleanup = new ContentTypeCleanup + { + PreventCleanup = source.HistoryCleanup.PreventCleanup, + KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays + }; + } + + target.Compositions = source.ContentTypeComposition.Select(contentType => new ContentTypeComposition + { + Key = contentType.Key, + CompositionType = contentType.Id == source.ParentId + ? ContentTypeCompositionType.Inheritance + : ContentTypeCompositionType.Composition + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 4844299fa3..fb12d7a5c4 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -1241,6 +1241,44 @@ } } }, + "/umbraco/management/api/v1/document-type/{key}": { + "get": { + "tags": [ + "Document Type" + ], + "operationId": "GetDocumentTypeByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentTypeModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, "/umbraco/management/api/v1/tree/document-type/children": { "get": { "tags": [ @@ -1385,6 +1423,44 @@ } } }, + "/umbraco/management/api/v1/document/{key}": { + "get": { + "tags": [ + "Document" + ], + "operationId": "GetDocumentByKey", + "parameters": [ + { + "name": "key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentModel" + } + ] + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + }, "/umbraco/management/api/v1/recycle-bin/document/children": { "get": { "tags": [ @@ -5465,6 +5541,16 @@ }, "additionalProperties": false }, + "ContentStateModel": { + "enum": [ + "NotCreated", + "Draft", + "Published", + "PublishedPendingChanges" + ], + "type": "integer", + "format": "int32" + }, "ContentTreeItemModel": { "required": [ "$type" @@ -5495,6 +5581,189 @@ } } }, + "ContentTypeCleanupModel": { + "type": "object", + "properties": { + "preventCleanup": { + "type": "boolean" + }, + "keepAllVersionsNewerThanDays": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "keepLatestVersionPerDayForDays": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "ContentTypeCompositionModel": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "compositionType": { + "$ref": "#/components/schemas/ContentTypeCompositionTypeModel" + } + }, + "additionalProperties": false + }, + "ContentTypeCompositionTypeModel": { + "enum": [ + "Composition", + "Inheritance" + ], + "type": "integer", + "format": "int32" + }, + "ContentTypeSortModel": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "sortOrder": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "ContentTypeViewModelBaseDocumentTypePropertyTypeDocumentTypePropertyTypeContainerModel": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "icon": { + "type": "string" + }, + "allowedAsRoot": { + "type": "boolean" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "isElement": { + "type": "boolean" + }, + "properties": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentTypePropertyTypeModel" + } + ] + } + }, + "containers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentTypePropertyTypeContainerModel" + } + ] + } + }, + "allowedContentTypes": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeSortModel" + } + ] + } + }, + "compositions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeCompositionModel" + } + ] + } + }, + "cleanup": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentTypeCleanupModel" + } + ] + } + }, + "additionalProperties": false + }, + "ContentUrlInfoModel": { + "type": "object", + "properties": { + "culture": { + "type": "string", + "nullable": true + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "ContentViewModelBaseDocumentValueDocumentVariantModel": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "contentTypeKey": { + "type": "string", + "format": "uuid" + }, + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentValueModel" + } + ] + } + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentVariantModel" + } + ] + } + } + }, + "additionalProperties": false + }, "CultureModel": { "type": "object", "properties": { @@ -5955,6 +6224,32 @@ } } }, + "DocumentModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ContentViewModelBaseDocumentValueDocumentVariantModel" + } + ], + "properties": { + "urls": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ContentUrlInfoModel" + } + ] + } + }, + "templateKey": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, "DocumentTreeItemModel": { "required": [ "$type" @@ -5987,6 +6282,47 @@ } } }, + "DocumentTypeModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ContentTypeViewModelBaseDocumentTypePropertyTypeDocumentTypePropertyTypeContainerModel" + } + ], + "properties": { + "allowedTemplateKeys": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "defaultTemplateKey": { + "type": "string", + "format": "uuid", + "nullable": true + } + }, + "additionalProperties": false + }, + "DocumentTypePropertyTypeContainerModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeContainerViewModelBaseModel" + } + ], + "additionalProperties": false + }, + "DocumentTypePropertyTypeModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PropertyTypeViewModelBaseModel" + } + ], + "additionalProperties": false + }, "DocumentTypeTreeItemModel": { "required": [ "$type" @@ -6013,6 +6349,34 @@ } } }, + "DocumentValueModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/ValueViewModelBaseModel" + } + ], + "additionalProperties": false + }, + "DocumentVariantModel": { + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/VariantViewModelBaseModel" + } + ], + "properties": { + "state": { + "$ref": "#/components/schemas/ContentStateModel" + }, + "publishDate": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, "EntityTreeItemModel": { "required": [ "$type" @@ -7345,6 +7709,111 @@ }, "additionalProperties": false }, + "PropertyTypeAppearanceModel": { + "type": "object", + "properties": { + "labelOnTop": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "PropertyTypeContainerViewModelBaseModel": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "parentKey": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "name": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string" + }, + "sortOrder": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "PropertyTypeValidationModel": { + "type": "object", + "properties": { + "mandatory": { + "type": "boolean" + }, + "mandatoryMessage": { + "type": "string", + "nullable": true + }, + "regEx": { + "type": "string", + "nullable": true + }, + "regExMessage": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "PropertyTypeViewModelBaseModel": { + "type": "object", + "properties": { + "key": { + "type": "string", + "format": "uuid" + }, + "containerKey": { + "type": "string", + "format": "uuid", + "nullable": true + }, + "alias": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string", + "nullable": true + }, + "dataTypeKey": { + "type": "string", + "format": "uuid" + }, + "variesByCulture": { + "type": "boolean" + }, + "variesBySegment": { + "type": "boolean" + }, + "validation": { + "oneOf": [ + { + "$ref": "#/components/schemas/PropertyTypeValidationModel" + } + ] + }, + "appearance": { + "oneOf": [ + { + "$ref": "#/components/schemas/PropertyTypeAppearanceModel" + } + ] + } + }, + "additionalProperties": false + }, "RecycleBinItemModel": { "required": [ "$type" @@ -8029,6 +8498,51 @@ }, "additionalProperties": false }, + "ValueViewModelBaseModel": { + "type": "object", + "properties": { + "culture": { + "type": "string", + "nullable": true + }, + "segment": { + "type": "string", + "nullable": true + }, + "alias": { + "type": "string" + }, + "value": { + "nullable": true + } + }, + "additionalProperties": false + }, + "VariantViewModelBaseModel": { + "type": "object", + "properties": { + "culture": { + "type": "string", + "nullable": true + }, + "segment": { + "type": "string", + "nullable": true + }, + "name": { + "type": "string" + }, + "createDate": { + "type": "string", + "format": "date-time" + }, + "updateDate": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false + }, "VersionModel": { "type": "object", "properties": { diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentState.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentState.cs new file mode 100644 index 0000000000..3bee0274d5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentState.cs @@ -0,0 +1,27 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +/// +/// The saved state of a content item +/// +public enum ContentState +{ + /// + /// The item isn't created yet + /// + NotCreated = 1, + + /// + /// The item is saved but isn't published + /// + Draft = 2, + + /// + /// The item is published and there are no pending changes + /// + Published = 3, + + /// + /// The item is published and there are pending changes + /// + PublishedPendingChanges = 4, +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentUrlInfo.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentUrlInfo.cs new file mode 100644 index 0000000000..87c233f593 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentUrlInfo.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public class ContentUrlInfo +{ + public required string? Culture { get; init; } + + public required string Url { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentViewModelBase.cs new file mode 100644 index 0000000000..8274e0a3e5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentViewModelBase.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public abstract class ContentViewModelBase + where TValueViewModelBase : ValueViewModelBase + where TVariantViewModel : VariantViewModelBase +{ + public Guid Key { get; set; } + + public Guid ContentTypeKey { get; set; } + + public IEnumerable Values { get; set; } = Array.Empty(); + + public IEnumerable Variants { get; set; } = Array.Empty(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ValueViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ValueViewModelBase.cs new file mode 100644 index 0000000000..9dacaa9f2b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ValueViewModelBase.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public abstract class ValueViewModelBase +{ + public string? Culture { get; set; } + + public string? Segment { get; set; } + + public string Alias { get; set; } = string.Empty; + + public object? Value { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/VariantViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/VariantViewModelBase.cs new file mode 100644 index 0000000000..cd5204582e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/VariantViewModelBase.cs @@ -0,0 +1,14 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public abstract class VariantViewModelBase +{ + public string? Culture { get; set; } + + public string? Segment { get; set; } + + public string Name { get; set; } = string.Empty; + + public DateTime CreateDate { get; set; } + + public DateTime UpdateDate { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCleanup.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCleanup.cs new file mode 100644 index 0000000000..072e2637c6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCleanup.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public class ContentTypeCleanup +{ + public bool PreventCleanup { get; init; } + + public int? KeepAllVersionsNewerThanDays { get; init; } + + public int? KeepLatestVersionPerDayForDays { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeComposition.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeComposition.cs new file mode 100644 index 0000000000..45fe7b463d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeComposition.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public class ContentTypeComposition +{ + public required Guid Key { get; init; } + + public required ContentTypeCompositionType CompositionType { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCompositionType.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCompositionType.cs new file mode 100644 index 0000000000..ad8479e05b --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeCompositionType.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public enum ContentTypeCompositionType +{ + Composition, + Inheritance +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeSort.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeSort.cs new file mode 100644 index 0000000000..4a43e108c0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeSort.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public class ContentTypeSort +{ + public required Guid Key { get; init; } + + public required int SortOrder { get; init; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeViewModelBase.cs new file mode 100644 index 0000000000..bddaf605b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/ContentTypeViewModelBase.cs @@ -0,0 +1,34 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public abstract class ContentTypeViewModelBase + where TPropertyType : PropertyTypeViewModelBase + where TPropertyTypeContainer : PropertyTypeContainerViewModelBase +{ + public Guid Key { get; set; } + + public string Alias { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public string Icon { get; set; } = string.Empty; + + public bool AllowedAsRoot { get; set; } + + public bool VariesByCulture { get; set; } + + public bool VariesBySegment { get; set; } + + public bool IsElement { get; set; } + + public IEnumerable Properties { get; set; } = Array.Empty(); + + public IEnumerable Containers { get; set; } = Array.Empty(); + + public IEnumerable AllowedContentTypes { get; set; } = Array.Empty(); + + public IEnumerable Compositions { get; set; } = Array.Empty(); + + public ContentTypeCleanup Cleanup { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeAppearance.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeAppearance.cs new file mode 100644 index 0000000000..4127e54aef --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeAppearance.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public class PropertyTypeAppearance +{ + public bool LabelOnTop { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeContainerViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeContainerViewModelBase.cs new file mode 100644 index 0000000000..049c959c0c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeContainerViewModelBase.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public abstract class PropertyTypeContainerViewModelBase +{ + public Guid Key { get; set; } + + public Guid? ParentKey { get; set; } + + public string? Name { get; set; } + + // NOTE: This needs to be a string because it can be anything in the future (= not necessarily limited to "tab" or "group") + public string Type { get; set; } = string.Empty; + + public int SortOrder { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeValidation.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeValidation.cs new file mode 100644 index 0000000000..2b4d6e536e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeValidation.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public class PropertyTypeValidation +{ + public bool Mandatory { get; set; } + + public string? MandatoryMessage { get; set; } + + public string? RegEx { get; set; } + + public string? RegExMessage { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeViewModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeViewModelBase.cs new file mode 100644 index 0000000000..51c50e1632 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/ContentType/PropertyTypeViewModelBase.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.ContentType; + +public abstract class PropertyTypeViewModelBase +{ + public Guid Key { get; set; } + + public Guid? ContainerKey { get; set; } + + public string Alias { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string? Description { get; set; } + + public Guid DataTypeKey { get; set; } + + public bool VariesByCulture { get; set; } + + public bool VariesBySegment { get; set; } + + public PropertyTypeValidation Validation { get; set; } = new(); + + public PropertyTypeAppearance Appearance { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueViewModel.cs new file mode 100644 index 0000000000..c8211460cb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueViewModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class DocumentValueViewModel : ValueViewModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantViewModel.cs new file mode 100644 index 0000000000..501778f03c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVariantViewModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class DocumentVariantViewModel : VariantViewModelBase +{ + public ContentState State { get; set; } + + public DateTime? PublishDate { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentViewModel.cs new file mode 100644 index 0000000000..a036205959 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentViewModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Api.Management.ViewModels.Content; + +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class DocumentViewModel : ContentViewModelBase +{ + public IEnumerable Urls { get; set; } = Array.Empty(); + + public Guid? TemplateKey { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeContainerViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeContainerViewModel.cs new file mode 100644 index 0000000000..1e51dd63c9 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeContainerViewModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; + +public class DocumentTypePropertyTypeContainerViewModel : PropertyTypeContainerViewModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeViewModel.cs new file mode 100644 index 0000000000..b48a85dac6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypePropertyTypeViewModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; + +public class DocumentTypePropertyTypeViewModel : PropertyTypeViewModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeViewModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeViewModel.cs new file mode 100644 index 0000000000..a89ba3d597 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentType/DocumentTypeViewModel.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Api.Management.ViewModels.ContentType; + +namespace Umbraco.Cms.Api.Management.ViewModels.DocumentType; + +public class DocumentTypeViewModel : ContentTypeViewModelBase +{ + public IEnumerable AllowedTemplateKeys { get; set; } = Array.Empty(); + + public Guid? DefaultTemplateKey { get; set; } +} diff --git a/src/Umbraco.Core/CompatibilitySuppressions.xml b/src/Umbraco.Core/CompatibilitySuppressions.xml index 7234ff16c8..213a91ab24 100644 --- a/src/Umbraco.Core/CompatibilitySuppressions.xml +++ b/src/Umbraco.Core/CompatibilitySuppressions.xml @@ -56,6 +56,13 @@ lib/net7.0/Umbraco.Core.dll true + + CP0002 + M:Umbraco.Cms.Core.Models.ContentTypeSort.#ctor(System.Lazy{System.Int32},System.Int32,System.String) + lib/net7.0/Umbraco.Core.dll + lib/net7.0/Umbraco.Core.dll + true + CP0002 M:Umbraco.Cms.Core.Models.DataType.get_Configuration diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index e10d650cac..802eab6ff4 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -21,16 +21,19 @@ public class ContentTypeSort : IValueObject, IDeepCloneable SortOrder = sortOrder; } - public ContentTypeSort(Lazy id, int sortOrder, string alias) + // FIXME: remove integer ID in constructor + public ContentTypeSort(Lazy id, Guid key, int sortOrder, string alias) { Id = id; SortOrder = sortOrder; Alias = alias; + Key = key; } /// /// Gets or sets the Id of the ContentType /// + // FIXME: remove this in favor of Key (Id should only be used at repository level) public Lazy Id { get; set; } = new(() => 0); /// @@ -43,6 +46,11 @@ public class ContentTypeSort : IValueObject, IDeepCloneable /// public string Alias { get; set; } = string.Empty; + /// + /// Gets or sets the unique Key of the ContentType + /// + public Guid Key { get; set; } + public object DeepClone() { var clone = (ContentTypeSort)MemberwiseClone(); diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 8e2a577f3a..4142b56a2f 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; @@ -10,13 +12,7 @@ namespace Umbraco.Extensions; public static class UrlProviderExtensions { - /// - /// Gets the URLs of the content item. - /// - /// - /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. - /// Contains all the URLs that we can figure out (based upon domains, etc). - /// + [Obsolete("Use GetContentUrlsAsync that takes ILanguageService instead of ILocalizationService. Will be removed in V15.")] public static async Task> GetContentUrlsAsync( this IContent content, IPublishedRouter publishedRouter, @@ -28,11 +24,40 @@ public static class UrlProviderExtensions ILogger logger, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider) + => await content.GetContentUrlsAsync( + publishedRouter, + umbracoContext, + StaticServiceProvider.Instance.GetRequiredService(), + textService, + contentService, + variationContextAccessor, + logger, + uriUtility, + publishedUrlProvider); + + /// + /// Gets the URLs of the content item. + /// + /// + /// Use when displaying URLs. If errors occur when generating the URLs, they will show in the list. + /// Contains all the URLs that we can figure out (based upon domains, etc). + /// + public static async Task> GetContentUrlsAsync( + this IContent content, + IPublishedRouter publishedRouter, + IUmbracoContext umbracoContext, + ILanguageService languageService, + ILocalizedTextService textService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILogger logger, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider) { ArgumentNullException.ThrowIfNull(content); ArgumentNullException.ThrowIfNull(publishedRouter); ArgumentNullException.ThrowIfNull(umbracoContext); - ArgumentNullException.ThrowIfNull(localizationService); + ArgumentNullException.ThrowIfNull(languageService); ArgumentNullException.ThrowIfNull(textService); ArgumentNullException.ThrowIfNull(contentService); ArgumentNullException.ThrowIfNull(variationContextAccessor); @@ -60,7 +85,7 @@ public static class UrlProviderExtensions // and, not only for those assigned to domains in the branch, because we want // to show what GetUrl() would return, for every culture. var urls = new HashSet(); - var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList(); + var cultures = (await languageService.GetAllAsync()).Select(x => x.IsoCode).ToList(); // get all URLs for all cultures // in a HashSet, so de-duplicates too diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 78beba5780..0b24a474b4 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -117,7 +117,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddScoped(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs index 9e582b6520..2c0ef3f383 100644 --- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs @@ -129,6 +129,7 @@ public class EntityMapDefinition : IMapDefinition private static void Map(EntityBasic source, ContentTypeSort target, MapperContext context) { target.Alias = source.Alias; + target.Key = source.Key; target.Id = new Lazy(() => Convert.ToInt32(source.Id)); } diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index ec4f2882bd..2f0350f5f5 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -1176,8 +1176,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging continue; } - allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), sortOrder, - allowedChild.Alias)); + allowedChildren?.Add(new ContentTypeSort(new Lazy(() => allowedChild.Id), allowedChild.Key, sortOrder, allowedChild.Alias)); sortOrder++; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 72ebd3a79a..ae7e875337 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -88,6 +88,7 @@ internal class ContentTypeCommonRepository : IContentTypeCommonRepository // prepare // note: same alias could be used for media, content... but always different ids = ok var aliases = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.Alias); + var keys = contentTypeDtos.ToDictionary(x => x.NodeId, x => x.NodeDto.UniqueId); // create var allowedDtoIx = 0; @@ -120,14 +121,17 @@ internal class ContentTypeCommonRepository : IContentTypeCommonRepository while (allowedDtoIx < allowedDtos?.Count && allowedDtos[allowedDtoIx].Id == contentTypeDto.NodeId) { ContentTypeAllowedContentTypeDto allowedDto = allowedDtos[allowedDtoIx]; - if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias)) + if (!aliases.TryGetValue(allowedDto.AllowedId, out var alias) + || !keys.TryGetValue(allowedDto.AllowedId, out Guid key)) { continue; } allowedContentTypes.Add(new ContentTypeSort( new Lazy(() => allowedDto.AllowedId), - allowedDto.SortOrder, alias!)); + key, + allowedDto.SortOrder, + alias!)); allowedDtoIx++; } diff --git a/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs new file mode 100644 index 0000000000..4583c502ab --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/ContextualJsonSerializer.cs @@ -0,0 +1,49 @@ +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Serialization; + +// FIXME: move away from Json.NET; this is a temporary fix that attempts to use System.Text.Json for management API operations, Json.NET for other operations +public class ContextualJsonSerializer : IJsonSerializer +{ + private readonly IRequestAccessor _requestAccessor; + private readonly IJsonSerializer _jsonNetSerializer; + private readonly IJsonSerializer _systemTextSerializer; + + public ContextualJsonSerializer(IRequestAccessor requestAccessor) + { + _requestAccessor = requestAccessor; + _jsonNetSerializer = new JsonNetSerializer(); + _systemTextSerializer = new SystemTextJsonSerializer(); + } + + public string Serialize(object? input) => ContextualizedSerializer().Serialize(input); + + public T? Deserialize(string input) => ContextualizedSerializer().Deserialize(input); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); + + private IJsonSerializer ContextualizedSerializer() + { + try + { + var requestedPath = _requestAccessor.GetRequestUrl()?.AbsolutePath; + if (requestedPath != null) + { + // add white listed paths for the System.Text.Json config serializer here + // - always use it for the new management API + if (requestedPath.Contains("/umbraco/management/api/")) + { + return _systemTextSerializer; + } + } + } + catch (Exception ex) + { + // ignore - this whole thing is a temporary workaround, let's not make a fuss + } + + return _jsonNetSerializer; + } +} + diff --git a/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs index 088a16be88..c7fcd456fa 100644 --- a/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/SystemTextConfigurationEditorJsonSerializer.cs @@ -1,10 +1,10 @@ using System.Text.Json; -using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Infrastructure.Serialization; -// TODO: clean up all config editor serializers when we can migrate fully to System.Text.Json +// FIXME: clean up all config editor serializers when we can migrate fully to System.Text.Json // - move this implementation to ConfigurationEditorJsonSerializer (delete the old implementation) // - use this implementation as the registered singleton (delete ContextualConfigurationEditorJsonSerializer) // - reuse the JsonObjectConverter implementation from management API (delete the local implementation - pending V12 branch update) @@ -21,9 +21,9 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ // in some cases, configs aren't camel cased in the DB, so we have to resort to case insensitive // property name resolving when creating configuration objects (deserializing DB configs) PropertyNameCaseInsensitive = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString + NumberHandling = JsonNumberHandling.AllowReadingFromString }; - _jsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); _jsonSerializerOptions.Converters.Add(new JsonObjectConverter()); } @@ -32,73 +32,4 @@ public class SystemTextConfigurationEditorJsonSerializer : IConfigurationEditorJ public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); - - // TODO: reuse the JsonObjectConverter implementation from management API - private class JsonObjectConverter : System.Text.Json.Serialization.JsonConverter - { - public override object Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) => - ParseObject(ref reader); - - public override void Write( - Utf8JsonWriter writer, - object objectToWrite, - JsonSerializerOptions options) - { - if (objectToWrite is null) - { - return; - } - - // If an object is equals "new object()", Json.Serialize would recurse forever and cause a stack overflow - // We have no good way of checking if its an empty object - // which is why we try to check if the object has any properties, and thus will be empty. - if (objectToWrite.GetType().Name is "Object" && !objectToWrite.GetType().GetProperties().Any()) - { - writer.WriteStartObject(); - writer.WriteEndObject(); - } - else - { - JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options); - } - } - - private object ParseObject(ref Utf8JsonReader reader) - { - if (reader.TokenType == JsonTokenType.StartArray) - { - var items = new List(); - while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) - { - items.Add(ParseObject(ref reader)); - } - - return items.ToArray(); - } - - if (reader.TokenType == JsonTokenType.StartObject) - { - var jsonNode = JsonNode.Parse(ref reader); - if (jsonNode is JsonObject jsonObject) - { - return jsonObject; - } - } - - return reader.TokenType switch - { - JsonTokenType.True => true, - JsonTokenType.False => false, - JsonTokenType.Number when reader.TryGetInt32(out int i) => i, - JsonTokenType.Number when reader.TryGetInt64(out long l) => l, - JsonTokenType.Number => reader.GetDouble(), - JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime, - JsonTokenType.String => reader.GetString()!, - _ => JsonDocument.ParseValue(ref reader).RootElement.Clone() - }; - } - } } diff --git a/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs new file mode 100644 index 0000000000..17a4e0fc1a --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Infrastructure.Serialization; + +public class SystemTextJsonSerializer : IJsonSerializer +{ + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public SystemTextJsonSerializer() + { + _jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + _jsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + // we may need to add JsonObjectConverter at some point, but for the time being things work fine without + // _jsonSerializerOptions.Converters.Add(new JsonObjectConverter()); + } + + public string Serialize(object? input) => JsonSerializer.Serialize(input, _jsonSerializerOptions); + + public T? Deserialize(string input) => JsonSerializer.Deserialize(input, _jsonSerializerOptions); + + public T? DeserializeSubset(string input, string key) => throw new NotSupportedException(); +} diff --git a/tests/Umbraco.TestData/LoadTestController.cs b/tests/Umbraco.TestData/LoadTestController.cs index 741fe4e94b..835723f38e 100644 --- a/tests/Umbraco.TestData/LoadTestController.cs +++ b/tests/Umbraco.TestData/LoadTestController.cs @@ -217,7 +217,7 @@ public class LoadTestController : Controller }; containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[] { - new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) + new ContentTypeSort(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) }); containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate }); containerType.SetDefaultTemplate(containerTemplate); diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs index 560e27defd..68fb1eb916 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs @@ -51,7 +51,8 @@ public class ContentTypeSortBuilder var id = _id ?? 1; var alias = _alias ?? Guid.NewGuid().ToString().ToCamelCase(); var sortOrder = _sortOrder ?? 0; + var key = Guid.NewGuid(); - return new ContentTypeSort(new Lazy(() => id), sortOrder, alias); + return new ContentTypeSort(new Lazy(() => id), key, sortOrder, alias); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs index aae69e2f61..63a6e2f585 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -809,8 +809,8 @@ public class ContentTypeRepositoryTest : UmbracoIntegrationTest var contentType = repository.Get(_simpleContentType.Id); contentType.AllowedContentTypes = new List { - new(new Lazy(() => subpageContentType.Id), 0, subpageContentType.Alias), - new(new Lazy(() => simpleSubpageContentType.Id), 1, simpleSubpageContentType.Alias) + new(new Lazy(() => subpageContentType.Id), subpageContentType.Key, 0, subpageContentType.Alias), + new(new Lazy(() => simpleSubpageContentType.Id), simpleSubpageContentType.Key, 1, simpleSubpageContentType.Alias) }; repository.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs index bafd0aa138..cf8658d64a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServicePerformanceTest.cs @@ -70,18 +70,18 @@ public class ContentServicePerformanceTest : UmbracoIntegrationTest ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 }); contentType1.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType2.Id), 0, contentType2.Alias), - new ContentTypeSort(new Lazy(() => contentType3.Id), 1, contentType3.Alias) + new ContentTypeSort(new Lazy(() => contentType2.Id), contentType2.Key, 0, contentType2.Alias), + new ContentTypeSort(new Lazy(() => contentType3.Id), contentType3.Key, 1, contentType3.Alias) }; contentType2.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType1.Id), 0, contentType1.Alias), - new ContentTypeSort(new Lazy(() => contentType3.Id), 1, contentType3.Alias) + new ContentTypeSort(new Lazy(() => contentType1.Id), contentType1.Key, 0, contentType1.Alias), + new ContentTypeSort(new Lazy(() => contentType3.Id), contentType3.Key, 1, contentType3.Alias) }; contentType3.AllowedContentTypes = new[] { - new ContentTypeSort(new Lazy(() => contentType1.Id), 0, contentType1.Alias), - new ContentTypeSort(new Lazy(() => contentType2.Id), 1, contentType2.Alias) + new ContentTypeSort(new Lazy(() => contentType1.Id), contentType1.Key, 0, contentType1.Alias), + new ContentTypeSort(new Lazy(() => contentType2.Id), contentType2.Key, 1, contentType2.Alias) }; ContentTypeService.Save(new[] { contentType1, contentType2, contentType3 }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs index 550fedcb41..0fe55ab779 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTagsTests.cs @@ -652,7 +652,7 @@ public class ContentServiceTagsTests : UmbracoIntegrationTest CreateAndAddTagsPropertyType(contentType); ContentTypeService.Save(contentType); contentType.AllowedContentTypes = - new[] { new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) }; + new[] { new ContentTypeSort(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) }; var content = ContentBuilder.CreateSimpleContent(contentType, "Tagged content"); content.AssignTags(PropertyEditorCollection, DataTypeService, Serializer, "tags", diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index 9d1242d5c7..cf977c5fcf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -1843,7 +1843,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id); contentType.AllowedContentTypes = new List { - new(new Lazy(() => contentType.Id), 0, contentType.Alias) + new(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) }; ContentTypeService.Save(contentType); @@ -1883,7 +1883,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ContentTypeBuilder.CreateSimpleContentType("umbTextpage1", "Textpage", defaultTemplateId: template.Id); contentType.AllowedContentTypes = new List { - new(new Lazy(() => contentType.Id), 0, contentType.Alias) + new(new Lazy(() => contentType.Id), contentType.Key, 0, contentType.Alias) }; ContentTypeService.Save(contentType); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 85f0f7439c..c5bee58962 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -139,7 +139,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest root.GetContentUrlsAsync( GetRequiredService(), GetRequiredService().GetRequiredUmbracoContext(), - GetRequiredService(), + GetRequiredService(), GetRequiredService(), ContentService, GetRequiredService(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs index 8f1fceb615..8a7f2ff66f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/GetContentUrlsTests.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -58,16 +55,16 @@ public class GetContentUrlsTests : PublishedSnapshotServiceTestBase return textService.Object; } - private ILocalizationService GetLangService(params string[] isoCodes) + private ILanguageService GetLangService(params string[] isoCodes) { var allLangs = isoCodes .Select(CultureInfo.GetCultureInfo) .Select(culture => new Language(culture.Name, culture.EnglishName) { IsDefault = true, IsMandatory = true }) .ToArray(); - var langServiceMock = new Mock(); - langServiceMock.Setup(x => x.GetAllLanguages()).Returns(allLangs); - langServiceMock.Setup(x => x.GetDefaultLanguageIsoCode()).Returns(allLangs.First(x => x.IsDefault).IsoCode); + var langServiceMock = new Mock(); + langServiceMock.Setup(x => x.GetAllAsync()).ReturnsAsync(allLangs); + langServiceMock.Setup(x => x.GetDefaultIsoCodeAsync()).ReturnsAsync(allLangs.First(x => x.IsDefault).IsoCode); return langServiceMock.Object; }