Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da43086017 | ||
|
|
852192a5d2 | ||
|
|
f4498c3d05 | ||
|
|
9b2fd1253b | ||
|
|
024697f3bf | ||
|
|
d920e93d1e | ||
|
|
853c1acf7e | ||
|
|
0ad020f0ce | ||
|
|
fc1455e0d8 | ||
|
|
15a8b4066a | ||
|
|
38f6e2b0da | ||
|
|
14f60a108a | ||
|
|
375a0b4388 | ||
|
|
6300ccc211 | ||
|
|
5b6f544d2a | ||
|
|
e932fa5404 | ||
|
|
83e580c3a7 | ||
|
|
31ee35e721 | ||
|
|
cf0f3f1380 | ||
|
|
31c3f5ae0c | ||
|
|
c38faec74b | ||
|
|
1e521760df | ||
|
|
8b759cf3a4 | ||
|
|
29c0151460 | ||
|
|
cc9c33bfe6 | ||
|
|
6edffd9f08 | ||
|
|
981f173a79 | ||
|
|
432dda8c47 | ||
|
|
a9fc88d7e6 | ||
|
|
7495c3c7b2 | ||
|
|
f9496e8067 | ||
|
|
7b10d39d66 | ||
|
|
97b3023e14 |
@@ -45,8 +45,8 @@
|
||||
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageVersion Include="Dazinator.Extensions.FileProviders" Version="2.0.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.0" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.0" />
|
||||
<PackageVersion Include="Examine" Version="3.7.1" />
|
||||
<PackageVersion Include="Examine.Core" Version="3.7.1" />
|
||||
<PackageVersion Include="HtmlAgilityPack" Version="1.11.74" />
|
||||
<PackageVersion Include="JsonPatch.Net" Version="3.1.1" />
|
||||
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
|
||||
@@ -100,4 +100,4 @@
|
||||
<!-- Examine.Lucene brings in a vulnerable version of Lucene.Net.Replicator -->
|
||||
<PackageVersion Include="Lucene.Net.Replicator" Version="4.8.0-beta00017" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -829,6 +829,8 @@ stages:
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to pre-release feed
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -890,6 +892,8 @@ stages:
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to NuGet
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
@@ -18,7 +18,12 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
.RequestServices
|
||||
.GetRequiredService<IRequestPreviewService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
|
||||
IApiAccessService apiAccessService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IApiAccessService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
|
||||
context.ResponseExpirationTimeSpan = _duration;
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
|
||||
public class ScaffoldDocumentBlueprintController : DocumentBlueprintControllerBase
|
||||
{
|
||||
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
|
||||
private readonly IUmbracoMapper _umbracoMapper;
|
||||
|
||||
public ScaffoldDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IUmbracoMapper umbracoMapper)
|
||||
{
|
||||
_contentBlueprintEditingService = contentBlueprintEditingService;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/scaffold")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(DocumentBlueprintResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Scaffold(CancellationToken cancellationToken, Guid id)
|
||||
{
|
||||
IContent? blueprint = await _contentBlueprintEditingService.GetScaffoldedAsync(id);
|
||||
return blueprint is not null
|
||||
? Ok(_umbracoMapper.Map<DocumentBlueprintResponseModel>(blueprint))
|
||||
: DocumentBlueprintNotFound();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Api.Management.Factories;
|
||||
@@ -5,11 +6,13 @@ using Umbraco.Cms.Api.Management.Routing;
|
||||
using Umbraco.Cms.Api.Management.ViewModels.Webhook.Logs;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Webhook.Logs;
|
||||
|
||||
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Webhook}")]
|
||||
[ApiExplorerSettings(GroupName = "Webhook")]
|
||||
[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)]
|
||||
public class WebhookLogControllerBase : ManagementApiControllerBase
|
||||
{
|
||||
protected PagedViewModel<WebhookLogResponseModel> CreatePagedWebhookLogResponseModel(PagedModel<WebhookLog> logs, IWebhookPresentationFactory webhookPresentationFactory)
|
||||
|
||||
@@ -3485,6 +3485,66 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/document-blueprint/{id}/scaffold": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Document Blueprint"
|
||||
],
|
||||
"operationId": "GetDocumentBlueprintByIdScaffold",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/DocumentBlueprintResponseModel"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ProblemDetails"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The resource is protected and requires an authentication token"
|
||||
},
|
||||
"403": {
|
||||
"description": "The authenticated user does not have access to this resource"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Backoffice User": [ ]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/document-blueprint/folder": {
|
||||
"post": {
|
||||
"tags": [
|
||||
|
||||
@@ -26,23 +26,36 @@
|
||||
<ProjectReference Include="..\Umbraco.Web.Website\Umbraco.Web.Website.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Restore and build backoffice project -->
|
||||
<!-- General ignored files -->
|
||||
<ItemGroup>
|
||||
<Content Remove="wwwroot\umbraco\assets\README.md" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<!-- BEGIN: Restore and build backoffice project -->
|
||||
<PropertyGroup>
|
||||
<BackofficeProjectDirectory Condition="'$(BackofficeProjectDirectory)' == ''">..\Umbraco.Web.UI.Client\</BackofficeProjectDirectory>
|
||||
<BackofficeAssetsPath>wwwroot\umbraco\backoffice</BackofficeAssetsPath>
|
||||
<BackofficeAssetsPath>$(ProjectDir)wwwroot\umbraco\backoffice</BackofficeAssetsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<BackofficeAssetsInputs Include="$(BackofficeProjectDirectory)package.json;$(BackofficeProjectDirectory)package-lock.json;$(BackofficeProjectDirectory)src\**" Exclude="$(DefaultItemExcludes)" />
|
||||
<Content Remove="$(BackofficeAssetsPath)\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="RestoreBackoffice" Inputs="$(BackofficeProjectDirectory)package-lock.json" Outputs="$(BackofficeProjectDirectory)node_modules\.package-lock.json">
|
||||
<Message Importance="high" Text="Restoring Backoffice NPM packages..." />
|
||||
<Exec Command="npm ci --no-fund --no-audit --prefer-offline" WorkingDirectory="$(BackofficeProjectDirectory)" />
|
||||
<Target Name="BuildStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
|
||||
<Message Text="Skip BuildBackoffice target because UmbracoBuild is '$(UmbracoBuild)' (this is not Visual Studio)" Importance="high" Condition="'$(UmbracoBuild)' != ''" />
|
||||
<Message Text="Skip BuildBackoffice target because '$(BackofficeAssetsPath)' already exists" Importance="high" Condition="Exists('$(BackofficeAssetsPath)')" />
|
||||
<Message Text="Call BuildBackoffice target because UmbracoBuild is empty (this is Visual Studio) and '$(BackofficeAssetsPath)' doesn't exist" Importance="high" Condition="'$(UmbracoBuild)' == '' and !Exists('$(BackofficeAssetsPath)')" />
|
||||
<CallTarget Targets="BuildBackoffice" Condition="'$(UmbracoBuild)' == '' and !Exists('$(BackofficeAssetsPath)')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildBackoffice" DependsOnTargets="RestoreBackoffice" BeforeTargets="AssignTargetPaths" Inputs="@(BackofficeAssetsInputs)" Outputs="$(IntermediateOutputPath)backoffice.complete.txt">
|
||||
<Target Name="RestoreBackoffice" Inputs="$(BackofficeProjectDirectory)package-lock.json" Outputs="$(BackofficeProjectDirectory)node_modules\.package-lock.json">
|
||||
<Message Importance="high" Text="Restoring Backoffice NPM packages..." />
|
||||
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(BackofficeProjectDirectory)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildBackoffice" DependsOnTargets="RestoreBackoffice">
|
||||
<Message Importance="high" Text="Executing Backoffice NPM build script..." />
|
||||
<Exec Command="npm run build:for:cms" WorkingDirectory="$(BackofficeProjectDirectory)" />
|
||||
<ItemGroup>
|
||||
@@ -61,27 +74,46 @@
|
||||
</DefineStaticWebAssets>
|
||||
</Target>
|
||||
|
||||
<!-- Restore and build login project -->
|
||||
<Target Name="CleanStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
|
||||
<Message Text="Skip CleanBackoffice target because '$(BackofficeAssetsPath)' doesn't exist" Importance="high" Condition="!Exists('$(BackofficeAssetsPath)')" />
|
||||
<Message Text="Skip CleanBackoffice target because preserve.backoffice marker file exists" Importance="high" Condition="Exists('$(BackofficeAssetsPath)') and Exists('$(SolutionDir)preserve.backoffice')" />
|
||||
<Message Text="Call CleanBackoffice target because '$(BackofficeAssetsPath)' exists and preserve.backoffice marker file doesn't exist" Importance="high" Condition="Exists('$(BackofficeAssetsPath)') and !Exists('$(SolutionDir)preserve.backoffice')" />
|
||||
<CallTarget Targets="CleanBackoffice" Condition="Exists('$(BackofficeAssetsPath)') and !Exists('$(SolutionDir)preserve.backoffice')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanBackoffice">
|
||||
<ItemGroup>
|
||||
<BackofficeDirectories Include="$(BackofficeAssetsPath)" />
|
||||
</ItemGroup>
|
||||
<RemoveDir Directories="@(BackofficeDirectories)" />
|
||||
</Target>
|
||||
<!-- END: Restore and build backoffice project -->
|
||||
|
||||
|
||||
|
||||
<!-- BEGIN: Restore and build login project -->
|
||||
<PropertyGroup>
|
||||
<LoginProjectDirectory Condition="'$(LoginProjectDirectory)' == ''">..\Umbraco.Web.UI.Login\</LoginProjectDirectory>
|
||||
<LoginAssetsPath>wwwroot\umbraco\login</LoginAssetsPath>
|
||||
<LoginAssetsPath>$(ProjectDir)wwwroot\umbraco\login</LoginAssetsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<LoginAssetsInputs Include="$(LoginProjectDirectory)**" Exclude="$(DefaultItemExcludes)" />
|
||||
<Content Remove="$(LoginAssetsPath)\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="wwwroot\umbraco\assets\README.md" />
|
||||
</ItemGroup>
|
||||
<Target Name="BuildLoginStaticAssetsPreconditions" BeforeTargets="AssignTargetPaths">
|
||||
<Message Text="Skip BuildLogin target because UmbracoBuild is '$(UmbracoBuild)' (this is not Visual Studio)" Importance="high" Condition="'$(UmbracoBuild)' != ''" />
|
||||
<Message Text="Skip BuildLogin target because '$(LoginAssetsPath)' already exists" Importance="high" Condition="Exists('$(LoginAssetsPath)')" />
|
||||
<Message Text="Call BuildLogin target because UmbracoBuild is empty (this is Visual Studio) and '$(LoginAssetsPath)' doesn't exist" Importance="high" Condition="'$(UmbracoBuild)' == '' and !Exists('$(LoginAssetsPath)')" />
|
||||
<CallTarget Targets="BuildLogin" Condition="'$(UmbracoBuild)' == '' and !Exists('$(LoginAssetsPath)')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="RestoreLogin" Inputs="$(LoginProjectDirectory)package-lock.json" Outputs="$(LoginProjectDirectory)node_modules/.package-lock.json">
|
||||
<Message Importance="high" Text="Restoring Login NPM packages..." />
|
||||
<Exec Command="npm ci --no-fund --no-audit --prefer-offline" WorkingDirectory="$(LoginProjectDirectory)" />
|
||||
<Exec Command="npm i --no-fund --no-audit" WorkingDirectory="$(LoginProjectDirectory)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin" BeforeTargets="AssignTargetPaths" Inputs="@(LoginAssetsInputs)" Outputs="$(IntermediateOutputPath)login.complete.txt">
|
||||
<Target Name="BuildLogin" DependsOnTargets="RestoreLogin">
|
||||
<Message Importance="high" Text="Executing Login NPM build script..." />
|
||||
<Exec Command="npm run build" WorkingDirectory="$(LoginProjectDirectory)" />
|
||||
<ItemGroup>
|
||||
@@ -99,4 +131,19 @@
|
||||
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
|
||||
</DefineStaticWebAssets>
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanLoginStaticAssetsPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
|
||||
<Message Text="Skip CleanLogin target because '$(LoginAssetsPath)' doesn't exist" Importance="high" Condition="!Exists('$(LoginAssetsPath)')" />
|
||||
<Message Text="Skip CleanLogin target because preserve.login marker file exists" Importance="high" Condition="Exists('$(LoginAssetsPath)') and Exists('$(SolutionDir)preserve.login')" />
|
||||
<Message Text="Call CleanLogin target because '$(LoginAssetsPath)' exists and preserve.login marker file doesn't exist" Importance="high" Condition="Exists('$(LoginAssetsPath)') and !Exists('$(SolutionDir)preserve.login')" />
|
||||
<CallTarget Targets="CleanLogin" Condition="Exists('$(LoginAssetsPath)') and !Exists('$(SolutionDir)preserve.login')" />
|
||||
</Target>
|
||||
|
||||
<Target Name="CleanLogin">
|
||||
<ItemGroup>
|
||||
<LoginDirectories Include="$(LoginAssetsPath)" />
|
||||
</ItemGroup>
|
||||
<RemoveDir Directories="@(LoginDirectories)" />
|
||||
</Target>
|
||||
<!-- END: Restore and build login project -->
|
||||
</Project>
|
||||
|
||||
@@ -22,4 +22,6 @@ public static class CacheKeys
|
||||
|
||||
public const string PreviewPropertyCacheKeyPrefix = "Cache.Property.CacheValues[D:";
|
||||
public const string PropertyCacheKeyPrefix = "Cache.Property.CacheValues[P:";
|
||||
|
||||
public const string MemberUserNameCachePrefix = "uRepo_userNameKey+";
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -21,9 +23,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly IPublishStatusManagementService _publishStatusManagementService;
|
||||
private readonly IIdKeyMap _idKeyMap;
|
||||
|
||||
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -38,6 +42,39 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
IContentService contentService,
|
||||
IPublishStatusManagementService publishStatusManagementService,
|
||||
IDocumentCacheService documentCacheService)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
domainService,
|
||||
eventAggregator,
|
||||
factory,
|
||||
documentUrlService,
|
||||
domainCacheService,
|
||||
documentNavigationQueryService,
|
||||
documentNavigationManagementService,
|
||||
contentService,
|
||||
publishStatusManagementService,
|
||||
documentCacheService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
|
||||
{
|
||||
}
|
||||
|
||||
public ContentCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IDomainService domainService,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IDocumentUrlService documentUrlService,
|
||||
IDomainCacheService domainCacheService,
|
||||
IDocumentNavigationQueryService documentNavigationQueryService,
|
||||
IDocumentNavigationManagementService documentNavigationManagementService,
|
||||
IContentService contentService,
|
||||
IPublishStatusManagementService publishStatusManagementService,
|
||||
IDocumentCacheService documentCacheService,
|
||||
ICacheManager cacheManager)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
@@ -49,6 +86,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
_contentService = contentService;
|
||||
_documentCacheService = documentCacheService;
|
||||
_publishStatusManagementService = publishStatusManagementService;
|
||||
|
||||
// TODO: Ideally we should inject IElementsCache
|
||||
// this interface is in infrastructure, and changing this is very breaking
|
||||
// so as long as we have the cache manager, which casts the IElementsCache to a simple AppCache we might as well use that.
|
||||
_cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
@@ -83,6 +125,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase<ContentCac
|
||||
AppCaches.RuntimeCache.ClearOfType<PublicAccessEntry>();
|
||||
AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey);
|
||||
|
||||
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
|
||||
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
|
||||
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
|
||||
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
|
||||
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
|
||||
_cacheManager.ElementsCache.Clear();
|
||||
|
||||
var idsRemoved = new HashSet<int>();
|
||||
IAppPolicyCache isolatedCache = AppCaches.IsolatedCaches.GetOrCreate<IContent>();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
@@ -18,7 +20,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
|
||||
[Obsolete("Use the constructor with ICacheManager instead, scheduled for removal in V17.")]
|
||||
public MediaCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
@@ -29,6 +33,31 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
IMediaNavigationManagementService mediaNavigationManagementService,
|
||||
IMediaService mediaService,
|
||||
IMediaCacheService mediaCacheService)
|
||||
: this(
|
||||
appCaches,
|
||||
serializer,
|
||||
idKeyMap,
|
||||
eventAggregator,
|
||||
factory,
|
||||
mediaNavigationQueryService,
|
||||
mediaNavigationManagementService,
|
||||
mediaService,
|
||||
mediaCacheService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ICacheManager>())
|
||||
{
|
||||
}
|
||||
|
||||
public MediaCacheRefresher(
|
||||
AppCaches appCaches,
|
||||
IJsonSerializer serializer,
|
||||
IIdKeyMap idKeyMap,
|
||||
IEventAggregator eventAggregator,
|
||||
ICacheRefresherNotificationFactory factory,
|
||||
IMediaNavigationQueryService mediaNavigationQueryService,
|
||||
IMediaNavigationManagementService mediaNavigationManagementService,
|
||||
IMediaService mediaService,
|
||||
IMediaCacheService mediaCacheService,
|
||||
ICacheManager cacheManager)
|
||||
: base(appCaches, serializer, eventAggregator, factory)
|
||||
{
|
||||
_idKeyMap = idKeyMap;
|
||||
@@ -36,6 +65,9 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
_mediaNavigationManagementService = mediaNavigationManagementService;
|
||||
_mediaService = mediaService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
|
||||
// TODO: Use IElementsCache instead of ICacheManager, see ContentCacheRefresher for more information.
|
||||
_cacheManager = cacheManager;
|
||||
}
|
||||
|
||||
#region Indirect
|
||||
@@ -87,6 +119,13 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase<MediaCacheRe
|
||||
AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey);
|
||||
Attempt<IAppPolicyCache?> mediaCache = AppCaches.IsolatedCaches.Get<IMedia>();
|
||||
|
||||
// Ideally, we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
|
||||
// The reason for this is that we have no way to know which elements are affected by the changes or what their keys are.
|
||||
// This is because currently published elements live exclusively in a JSON blob in the umbracoPropertyData table.
|
||||
// This means that the only way to resolve these keys is to actually parse this data with a specific value converter, and for all cultures, which is not possible.
|
||||
// If published elements become their own entities with relations, instead of just property data, we can revisit this.
|
||||
_cacheManager.ElementsCache.Clear();
|
||||
|
||||
foreach (JsonPayload payload in payloads)
|
||||
{
|
||||
if (payload.ChangeTypes == TreeChangeTypes.Remove)
|
||||
|
||||
@@ -71,11 +71,22 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
foreach (JsonPayload p in payloads)
|
||||
{
|
||||
_idKeyMap.ClearCache(p.Id);
|
||||
if (memberCache.Success)
|
||||
if (memberCache.Success is false)
|
||||
{
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
|
||||
continue;
|
||||
}
|
||||
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, int>(p.Id));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.Username));
|
||||
|
||||
// This specific cache key was introduced to fix an issue where the member username could not be the same as the member id, because the cache keys collided.
|
||||
// This is done in a bit of a hacky way, because the cache key is created internally in the repository, but we need to clear it here.
|
||||
// Ideally, we want to use a shared way of generating the key between this and the repository.
|
||||
// Additionally, the RepositoryCacheKeys actually caches the string to avoid re-allocating memory; we would like to also use this in the repository
|
||||
// See:
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17350
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17815
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@ namespace Umbraco.Extensions;
|
||||
public static class ContentSettingsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if file extension is allowed for upload based on (optional) white list and black list
|
||||
/// held in settings.
|
||||
/// Allow upload if extension is whitelisted OR if there is no whitelist and extension is NOT blacklisted.
|
||||
/// Determines if file extension is allowed for upload based on (optional) allow list and deny list held in settings.
|
||||
/// Disallowed file extensions are only considered if there are no allowed file extensions.
|
||||
/// </summary>
|
||||
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension) =>
|
||||
contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) ||
|
||||
(contentSettings.AllowedUploadedFileExtensions.Any() == false &&
|
||||
contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension)) == false);
|
||||
/// <param name="contentSettings">The content settings.</param>
|
||||
/// <param name="extension">The file extension.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the file extension is allowed for upload; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsFileAllowedForUpload(this ContentSettings contentSettings, string extension)
|
||||
=> contentSettings.AllowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) ||
|
||||
(contentSettings.AllowedUploadedFileExtensions.Any() == false && contentSettings.DisallowedUploadedFileExtensions.Any(x => x.InvariantEquals(extension.Trim())) == false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auto-fill configuration for a specified property alias.
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
<key alias="invalidEmpty">Value cannot be empty</key>
|
||||
<key alias="invalidPattern">Value is invalid, it does not match the correct pattern</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMemberGroupName">Invalid member group name</key>
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
|
||||
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, you have entered <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="stringLengthExceeded">The string length exceeds the maximum length of %0% characters, %1% too many.</key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
<key alias="invalidMediaType">The chosen media type is invalid.</key>
|
||||
|
||||
@@ -245,13 +245,13 @@ public static class ContentExtensions
|
||||
}
|
||||
|
||||
IEnumerable<ContentSchedule> expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire);
|
||||
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date))
|
||||
if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.UtcNow > x.Date))
|
||||
{
|
||||
return ContentStatus.Expired;
|
||||
}
|
||||
|
||||
IEnumerable<ContentSchedule> release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release);
|
||||
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now))
|
||||
if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.UtcNow))
|
||||
{
|
||||
return ContentStatus.AwaitingRelease;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.HostedServices;
|
||||
public interface IBackgroundTaskQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Enqueue a work item to be executed on in the background.
|
||||
/// Enqueue a work item to be executed in the background.
|
||||
/// </summary>
|
||||
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
|
||||
|
||||
|
||||
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
|
||||
|
||||
// nothing prevents us to reach the file, security-wise, yet it is outside
|
||||
// this filesystem's root - throw
|
||||
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
|
||||
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
@@ -35,6 +37,21 @@ internal sealed class ContentBlueprintEditingService
|
||||
return await Task.FromResult(blueprint);
|
||||
}
|
||||
|
||||
public Task<IContent?> GetScaffoldedAsync(Guid key)
|
||||
{
|
||||
IContent? blueprint = ContentService.GetBlueprintById(key);
|
||||
if (blueprint is null)
|
||||
{
|
||||
return Task.FromResult<IContent?>(null);
|
||||
}
|
||||
|
||||
using ICoreScope scope = CoreScopeProvider.CreateCoreScope();
|
||||
scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages()));
|
||||
scope.Complete();
|
||||
|
||||
return Task.FromResult<IContent?>(blueprint);
|
||||
}
|
||||
|
||||
public async Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take)
|
||||
{
|
||||
IContentType? contentType = await ContentTypeService.GetAsync(contentTypeKey);
|
||||
|
||||
@@ -44,26 +44,51 @@ public class DocumentUrlService : IDocumentUrlService
|
||||
/// <summary>
|
||||
/// Model used to cache a single published document along with all it's URL segments.
|
||||
/// </summary>
|
||||
private class PublishedDocumentUrlSegments
|
||||
/// <remarks>Internal for the purpose of unit and benchmark testing.</remarks>
|
||||
internal class PublishedDocumentUrlSegments
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the document key.
|
||||
/// </summary>
|
||||
public required Guid DocumentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the language Id.
|
||||
/// </summary>
|
||||
public required int LanguageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of <see cref="UrlSegment"/> for the document, language and state.
|
||||
/// </summary>
|
||||
public required IList<UrlSegment> UrlSegments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the document is a draft version or not.
|
||||
/// </summary>
|
||||
public required bool IsDraft { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Model used to represent a URL segment for a document in the cache.
|
||||
/// </summary>
|
||||
public class UrlSegment
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UrlSegment"/> class.
|
||||
/// </summary>
|
||||
public UrlSegment(string segment, bool isPrimary)
|
||||
{
|
||||
Segment = segment;
|
||||
IsPrimary = isPrimary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL segment string.
|
||||
/// </summary>
|
||||
public string Segment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this URL segment is the primary one for the document, language and state.
|
||||
/// </summary>
|
||||
public bool IsPrimary { get; }
|
||||
}
|
||||
}
|
||||
@@ -168,45 +193,40 @@ public class DocumentUrlService : IDocumentUrlService
|
||||
scope.Complete();
|
||||
}
|
||||
|
||||
private static IEnumerable<PublishedDocumentUrlSegments> ConvertToCacheModel(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
|
||||
/// <summary>
|
||||
/// Converts a collection of <see cref="PublishedDocumentUrlSegment"/> to a collection of <see cref="PublishedDocumentUrlSegments"/> for caching purposes.
|
||||
/// </summary>
|
||||
/// <param name="publishedDocumentUrlSegments">The collection of <see cref="PublishedDocumentUrlSegment"/> retrieved from the database on startup.</param>
|
||||
/// <returns>The collection of cache models.</returns>
|
||||
/// <remarks>Internal for the purpose of unit and benchmark testing.</remarks>
|
||||
internal static IEnumerable<PublishedDocumentUrlSegments> ConvertToCacheModel(IEnumerable<PublishedDocumentUrlSegment> publishedDocumentUrlSegments)
|
||||
{
|
||||
var cacheModels = new List<PublishedDocumentUrlSegments>();
|
||||
var cacheModels = new Dictionary<(Guid DocumentKey, int LanguageId, bool IsDraft), PublishedDocumentUrlSegments>();
|
||||
|
||||
foreach (PublishedDocumentUrlSegment model in publishedDocumentUrlSegments)
|
||||
{
|
||||
PublishedDocumentUrlSegments? existingCacheModel = GetModelFromCache(cacheModels, model);
|
||||
if (existingCacheModel is null)
|
||||
(Guid DocumentKey, int LanguageId, bool IsDraft) key = (model.DocumentKey, model.LanguageId, model.IsDraft);
|
||||
|
||||
if (!cacheModels.TryGetValue(key, out PublishedDocumentUrlSegments? existingCacheModel))
|
||||
{
|
||||
cacheModels.Add(new PublishedDocumentUrlSegments
|
||||
cacheModels[key] = new PublishedDocumentUrlSegments
|
||||
{
|
||||
DocumentKey = model.DocumentKey,
|
||||
LanguageId = model.LanguageId,
|
||||
UrlSegments = [new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary)],
|
||||
IsDraft = model.IsDraft,
|
||||
});
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
existingCacheModel.UrlSegments = GetUpdatedUrlSegments(existingCacheModel.UrlSegments, model.UrlSegment, model.IsPrimary);
|
||||
if (existingCacheModel.UrlSegments.Any(x => x.Segment == model.UrlSegment) is false)
|
||||
{
|
||||
existingCacheModel.UrlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(model.UrlSegment, model.IsPrimary));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cacheModels;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static PublishedDocumentUrlSegments? GetModelFromCache(List<PublishedDocumentUrlSegments> cacheModels, PublishedDocumentUrlSegment model)
|
||||
=> cacheModels
|
||||
.SingleOrDefault(x => x.DocumentKey == model.DocumentKey && x.LanguageId == model.LanguageId && x.IsDraft == model.IsDraft);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static IList<PublishedDocumentUrlSegments.UrlSegment> GetUpdatedUrlSegments(IList<PublishedDocumentUrlSegments.UrlSegment> urlSegments, string segment, bool isPrimary)
|
||||
{
|
||||
if (urlSegments.FirstOrDefault(x => x.Segment == segment) is null)
|
||||
{
|
||||
urlSegments.Add(new PublishedDocumentUrlSegments.UrlSegment(segment, isPrimary));
|
||||
}
|
||||
|
||||
return urlSegments;
|
||||
return cacheModels.Values;
|
||||
}
|
||||
|
||||
private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode, bool isDraft)
|
||||
|
||||
@@ -8,6 +8,8 @@ public interface IContentBlueprintEditingService
|
||||
{
|
||||
Task<IContent?> GetAsync(Guid key);
|
||||
|
||||
Task<IContent?> GetScaffoldedAsync(Guid key) => Task.FromResult<IContent?>(null);
|
||||
|
||||
Task<Attempt<PagedModel<IContent>?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(
|
||||
Guid contentTypeKey,
|
||||
int skip,
|
||||
|
||||
@@ -9,4 +9,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
|
||||
[Obsolete("This has been relocated into Umbraco.Cms.Core. This definition in Umbraco.Cms.Infrastructure is scheduled for removal in Umbraco 17.")]
|
||||
public interface IBackgroundTaskQueue : Core.HostedServices.IBackgroundTaskQueue
|
||||
{
|
||||
/// <summary>
|
||||
/// Enqueue a work item to be executed in the background.
|
||||
/// </summary>
|
||||
void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
|
||||
|
||||
/// <summary>
|
||||
/// Dequeue the first item on the queue.
|
||||
/// </summary>
|
||||
Task<Func<CancellationToken, Task>?> DequeueAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ internal class AccessDto
|
||||
[ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")]
|
||||
public int NoAccessNodeId { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ internal class AccessRuleDto
|
||||
[Column("ruleType")]
|
||||
public string? RuleType { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ internal class AuditEntryDto
|
||||
[Length(Constants.Audit.IpLength)]
|
||||
public string? PerformingIp { get; set; }
|
||||
|
||||
[Column("eventDateUtc")]
|
||||
[Column("eventDateUtc", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime EventDateUtc { get; set; }
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ConsentDto
|
||||
[Length(512)]
|
||||
public string? Action { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ internal class ContentScheduleDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)] // can be invariant
|
||||
public int? LanguageId { get; set; }
|
||||
|
||||
// NOTE: this date is explicitly stored and treated as UTC despite the lack of "Utc" postfix.
|
||||
[Column("date")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@ internal class ContentVersionCleanupPolicyDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public int? KeepLatestVersionPerDayForDays { get; set; }
|
||||
|
||||
[Column("updated")]
|
||||
[Column("updated", ForceToUtc = false)]
|
||||
public DateTime Updated { get; set; }
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ internal class ContentVersionCultureVariationDto
|
||||
[Column("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[Column("date")] // TODO: db rename to 'updateDate'
|
||||
[Column("date", ForceToUtc = false)] // TODO: db rename to 'updateDate'
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
[Column("availableUserId")] // TODO: db rename to 'updateDate'
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ContentVersionDto
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId,preventCleanup")]
|
||||
public int NodeId { get; set; }
|
||||
|
||||
[Column("versionDate")] // TODO: db rename to 'updateDate'
|
||||
[Column("versionDate", ForceToUtc = false)] // TODO: db rename to 'updateDate'
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime VersionDate { get; set; }
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class CreatedPackageSchemaDto
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Value { get; set; } = null!;
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
|
||||
@@ -20,6 +20,6 @@ internal class DocumentPublishedReadOnlyDto
|
||||
[Column("newest")]
|
||||
public bool Newest { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
public DateTime VersionDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ internal class ExternalLoginDto
|
||||
[Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")]
|
||||
public string ProviderKey { get; set; } = null!;
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ internal class ExternalLoginTokenDto
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Value { get; set; } = null!;
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ internal class KeyValueDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public string? Value { get; set; }
|
||||
|
||||
[Column("updated")]
|
||||
[Column("updated", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ internal class LogDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public string? EntityType { get; set; }
|
||||
|
||||
[Column("Datestamp")]
|
||||
[Column("Datestamp", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_datestamp", ForColumns = "Datestamp,userId,NodeId")]
|
||||
public DateTime Datestamp { get; set; }
|
||||
|
||||
@@ -45,7 +45,7 @@ internal class MemberDto
|
||||
[Length(255)]
|
||||
public string? SecurityStampToken { get; set; }
|
||||
|
||||
[Column("emailConfirmedDate")]
|
||||
[Column("emailConfirmedDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? EmailConfirmedDate { get; set; }
|
||||
|
||||
@@ -62,15 +62,15 @@ internal class MemberDto
|
||||
[Constraint(Default = 1)]
|
||||
public bool IsApproved { get; set; }
|
||||
|
||||
[Column("lastLoginDate")]
|
||||
[Column("lastLoginDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
|
||||
[Column("lastLockoutDate")]
|
||||
[Column("lastLockoutDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLockoutDate { get; set; }
|
||||
|
||||
[Column("lastPasswordChangeDate")]
|
||||
[Column("lastPasswordChangeDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastPasswordChangeDate { get; set; }
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ public class NodeDto
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")]
|
||||
public Guid? NodeObjectType { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ internal class RelationDto
|
||||
[ForeignKey(typeof(RelationTypeDto))]
|
||||
public int RelationType { get; set; }
|
||||
|
||||
[Column("datetime")]
|
||||
[Column("datetime", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime Datetime { get; set; }
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ internal class ServerRegistrationDto
|
||||
[Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique
|
||||
public string? ServerIdentity { get; set; }
|
||||
|
||||
[Column("registeredDate")]
|
||||
[Column("registeredDate", ForceToUtc = false)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime DateRegistered { get; set; }
|
||||
|
||||
[Column("lastNotifiedDate")]
|
||||
[Column("lastNotifiedDate", ForceToUtc = false)]
|
||||
public DateTime DateAccessed { get; set; }
|
||||
|
||||
[Column("isActive")]
|
||||
|
||||
@@ -73,32 +73,32 @@ public class UserDto
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public int? FailedLoginAttempts { get; set; }
|
||||
|
||||
[Column("lastLockoutDate")]
|
||||
[Column("lastLockoutDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLockoutDate { get; set; }
|
||||
|
||||
[Column("lastPasswordChangeDate")]
|
||||
[Column("lastPasswordChangeDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastPasswordChangeDate { get; set; }
|
||||
|
||||
[Column("lastLoginDate")]
|
||||
[Column("lastLoginDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? LastLoginDate { get; set; }
|
||||
|
||||
[Column("emailConfirmedDate")]
|
||||
[Column("emailConfirmedDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? EmailConfirmedDate { get; set; }
|
||||
|
||||
[Column("invitedDate")]
|
||||
[Column("invitedDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.Null)]
|
||||
public DateTime? InvitedDate { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; } = DateTime.Now;
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; } = DateTime.Now;
|
||||
|
||||
@@ -44,12 +44,12 @@ public class UserGroupDto
|
||||
[Obsolete("Is not used anymore Use UserGroup2PermissionDtos instead. This will be removed in Umbraco 18.")]
|
||||
public string? DefaultPermissions { get; set; }
|
||||
|
||||
[Column("createDate")]
|
||||
[Column("createDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
[Column("updateDate")]
|
||||
[Column("updateDate", ForceToUtc = false)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Constraint(Default = SystemMethods.CurrentDateTime)]
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using NPoco;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
|
||||
@@ -24,7 +24,7 @@ internal class WebhookLogDto
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string StatusCode { get; set; } = string.Empty;
|
||||
|
||||
[Column(Name = "date")]
|
||||
[Column(Name = "date", ForceToUtc = false)]
|
||||
[Index(IndexTypes.NonClustered, Name = "IX_" + Constants.DatabaseSchema.Tables.WebhookLog + "_date")]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
@@ -39,7 +39,6 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
private readonly ITagRepository _tagRepository;
|
||||
private bool _passwordConfigInitialized;
|
||||
private string? _passwordConfigJson;
|
||||
private const string UsernameCacheKey = "uRepo_userNameKey+";
|
||||
|
||||
public MemberRepository(
|
||||
IScopeAccessor scopeAccessor,
|
||||
@@ -327,7 +326,7 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
}
|
||||
|
||||
public IMember? GetByUsername(string? username) =>
|
||||
_memberByUsernameCachePolicy.GetByUserName(UsernameCacheKey, username, PerformGetByUsername, PerformGetAllByUsername);
|
||||
_memberByUsernameCachePolicy.GetByUserName(CacheKeys.MemberUserNameCachePrefix, username, PerformGetByUsername, PerformGetAllByUsername);
|
||||
|
||||
public int[] GetMemberIds(string[] usernames)
|
||||
{
|
||||
@@ -609,7 +608,7 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
|
||||
protected override void PersistDeletedItem(IMember entity)
|
||||
{
|
||||
_memberByUsernameCachePolicy.DeleteByUserName(UsernameCacheKey, entity.Username);
|
||||
_memberByUsernameCachePolicy.DeleteByUserName(CacheKeys.MemberUserNameCachePrefix, entity.Username);
|
||||
base.PersistDeletedItem(entity);
|
||||
}
|
||||
|
||||
@@ -943,7 +942,7 @@ public class MemberRepository : ContentRepositoryBase<int, IMember, MemberReposi
|
||||
|
||||
OnUowRefreshedEntity(new MemberRefreshNotification(entity, new EventMessages()));
|
||||
|
||||
_memberByUsernameCachePolicy.DeleteByUserName(UsernameCacheKey, entity.Username);
|
||||
_memberByUsernameCachePolicy.DeleteByUserName(CacheKeys.MemberUserNameCachePrefix, entity.Username);
|
||||
|
||||
entity.ResetDirtyProperties();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
@@ -291,6 +292,11 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
if (canUpdateInvariantData is false && targetValue is null)
|
||||
{
|
||||
return sourceValue;
|
||||
}
|
||||
|
||||
BlockEditorData<TValue, TLayout>? source = BlockEditorValues.DeserializeAndClean(sourceValue);
|
||||
BlockEditorData<TValue, TLayout>? target = BlockEditorValues.DeserializeAndClean(targetValue);
|
||||
|
||||
@@ -310,31 +316,29 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
source = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
|
||||
var mergedInvariant = UpdateSourceInvariantData(source, target, canUpdateInvariantData);
|
||||
|
||||
if (source is null && target is null)
|
||||
// if the structure (invariant) is not defined after merger, the target content does not matter
|
||||
if (mergedInvariant is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (source is null && target?.Layout is not null)
|
||||
// since we merged the invariant data (layout) before we get to this point
|
||||
// we just need an empty valid object to run comparisons at this point
|
||||
if (source is null)
|
||||
{
|
||||
source = new BlockEditorData<TValue, TLayout>([], CreateWithLayout(target.Layout));
|
||||
}
|
||||
else if (target is null && source?.Layout is not null)
|
||||
{
|
||||
target = new BlockEditorData<TValue, TLayout>([], CreateWithLayout(source.Layout));
|
||||
source = new BlockEditorData<TValue, TLayout>([], new TValue());
|
||||
}
|
||||
|
||||
// at this point the layout should have been merged or fallback created
|
||||
if (source is null || target is null)
|
||||
{
|
||||
throw new ArgumentException("invalid sourceValue or targetValue");
|
||||
}
|
||||
// update the target with the merged invariant
|
||||
target!.BlockValue.Layout = mergedInvariant.BlockValue.Layout;
|
||||
|
||||
// remove all the blocks that are no longer part of the layout
|
||||
target.BlockValue.ContentData.RemoveAll(contentBlock =>
|
||||
target.Layout!.Any(layoutItem => layoutItem.ReferencesContent(contentBlock.Key)) is false);
|
||||
// remove any exposes that no longer have content assigned to them
|
||||
target.BlockValue.Expose.RemoveAll(expose => target.BlockValue.ContentData.Any(data => data.Key == expose.ContentKey) is false);
|
||||
|
||||
target.BlockValue.SettingsData.RemoveAll(settingsBlock =>
|
||||
target.Layout!.Any(layoutItem => layoutItem.ReferencesSetting(settingsBlock.Key)) is false);
|
||||
@@ -342,16 +346,80 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
CleanupVariantValues(source.BlockValue.ContentData, target.BlockValue.ContentData, canUpdateInvariantData, allowedCultures);
|
||||
CleanupVariantValues(source.BlockValue.SettingsData, target.BlockValue.SettingsData, canUpdateInvariantData, allowedCultures);
|
||||
|
||||
// every source block value for a culture that is not allowed to be edited should be present on the target
|
||||
RestoreMissingValues(
|
||||
source.BlockValue.ContentData,
|
||||
target.BlockValue.ContentData,
|
||||
mergedInvariant.Layout!,
|
||||
(layoutItem, itemData) => layoutItem.ContentKey == itemData.Key,
|
||||
canUpdateInvariantData,
|
||||
allowedCultures);
|
||||
RestoreMissingValues(
|
||||
source.BlockValue.SettingsData,
|
||||
target.BlockValue.SettingsData,
|
||||
mergedInvariant.Layout!,
|
||||
(layoutItem, itemData) => layoutItem.SettingsKey == itemData.Key,
|
||||
canUpdateInvariantData,
|
||||
allowedCultures);
|
||||
|
||||
// update the expose list from source for any blocks that were restored
|
||||
var missingSourceExposes =
|
||||
source.BlockValue.Expose.Where(sourceExpose =>
|
||||
target.BlockValue.Expose.Any(targetExpose => targetExpose.ContentKey == sourceExpose.ContentKey) is false
|
||||
&& target.BlockValue.ContentData.Any(data => data.Key == sourceExpose.ContentKey)).ToList();
|
||||
foreach (BlockItemVariation missingSourceExpose in missingSourceExposes)
|
||||
{
|
||||
target.BlockValue.Expose.Add(missingSourceExpose);
|
||||
}
|
||||
|
||||
return target.BlockValue;
|
||||
}
|
||||
|
||||
private void RestoreMissingValues(
|
||||
List<BlockItemData> sourceBlockItemData,
|
||||
List<BlockItemData> targetBlockItemData,
|
||||
IEnumerable<TLayout> mergedLayout,
|
||||
Func<TLayout, BlockItemData, bool> relevantBlockItemMatcher,
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
IEnumerable<BlockItemData> blockItemsToCheck = sourceBlockItemData.Where(itemData =>
|
||||
mergedLayout.Any(layoutItem => relevantBlockItemMatcher(layoutItem, itemData)));
|
||||
foreach (BlockItemData blockItemData in blockItemsToCheck)
|
||||
{
|
||||
var relevantValues = blockItemData.Values.Where(value =>
|
||||
(value.Culture is null && canUpdateInvariantData is false)
|
||||
|| (value.Culture is not null && allowedCultures.Contains(value.Culture) is false)).ToList();
|
||||
if (relevantValues.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockItemData targetBlockData =
|
||||
targetBlockItemData.FirstOrDefault(itemData => itemData.Key == blockItemData.Key)
|
||||
?? new BlockItemData(blockItemData.Key, blockItemData.ContentTypeKey, blockItemData.ContentTypeAlias);
|
||||
foreach (BlockPropertyValue missingValue in relevantValues.Where(value => targetBlockData.Values.Any(targetValue =>
|
||||
targetValue.Alias == value.Alias
|
||||
&& targetValue.Culture == value.Culture
|
||||
&& targetValue.Segment == value.Segment) is false))
|
||||
{
|
||||
targetBlockData.Values.Add(missingValue);
|
||||
}
|
||||
|
||||
if (targetBlockItemData.Any(existingBlockItemData => existingBlockItemData.Key == targetBlockData.Key) is false)
|
||||
{
|
||||
targetBlockItemData.Add(blockItemData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupVariantValues(
|
||||
List<BlockItemData> sourceBlockItems,
|
||||
List<BlockItemData> targetBlockItems,
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
// merge the source values into the target values for culture
|
||||
// merge the source values into the target values per culture
|
||||
foreach (BlockItemData targetBlockItem in targetBlockItems)
|
||||
{
|
||||
BlockItemData? sourceBlockItem = sourceBlockItems.FirstOrDefault(i => i.Key == targetBlockItem.Key);
|
||||
|
||||
+2
-27
@@ -25,65 +25,40 @@ internal sealed class CacheRefreshingNotificationHandler :
|
||||
{
|
||||
private readonly IDocumentCacheService _documentCacheService;
|
||||
private readonly IMediaCacheService _mediaCacheService;
|
||||
private readonly IElementsCache _elementsCache;
|
||||
private readonly IRelationService _relationService;
|
||||
private readonly IPublishedContentTypeCache _publishedContentTypeCache;
|
||||
|
||||
public CacheRefreshingNotificationHandler(
|
||||
IDocumentCacheService documentCacheService,
|
||||
IMediaCacheService mediaCacheService,
|
||||
IElementsCache elementsCache,
|
||||
IRelationService relationService,
|
||||
IPublishedContentTypeCache publishedContentTypeCache)
|
||||
{
|
||||
_documentCacheService = documentCacheService;
|
||||
_mediaCacheService = mediaCacheService;
|
||||
_elementsCache = elementsCache;
|
||||
_relationService = relationService;
|
||||
_publishedContentTypeCache = publishedContentTypeCache;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(ContentRefreshNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ClearElementsCache();
|
||||
|
||||
await _documentCacheService.RefreshContentAsync(notification.Entity);
|
||||
}
|
||||
=> await _documentCacheService.RefreshContentAsync(notification.Entity);
|
||||
|
||||
public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IContent deletedEntity in notification.DeletedEntities)
|
||||
{
|
||||
ClearElementsCache();
|
||||
await _documentCacheService.DeleteItemAsync(deletedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleAsync(MediaRefreshNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
ClearElementsCache();
|
||||
await _mediaCacheService.RefreshMediaAsync(notification.Entity);
|
||||
}
|
||||
=> await _mediaCacheService.RefreshMediaAsync(notification.Entity);
|
||||
|
||||
public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IMedia deletedEntity in notification.DeletedEntities)
|
||||
{
|
||||
ClearElementsCache();
|
||||
await _mediaCacheService.DeleteItemAsync(deletedEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearElementsCache()
|
||||
{
|
||||
// Ideally we'd like to not have to clear the entire cache here. However, this was the existing behavior in NuCache.
|
||||
// The reason for this is that we have no way to know which elements are affected by the changes. or what their keys are.
|
||||
// This is because currently published elements lives exclusively in a JSON blob in the umbracoPropertyData table.
|
||||
// This means that the only way to resolve these keys are to actually parse this data with a specific value converter, and for all cultures, which is not feasible.
|
||||
// If published elements become their own entities with relations, instead of just property data, we can revisit this,
|
||||
_elementsCache.Clear();
|
||||
}
|
||||
|
||||
public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
const ContentTypeChangeTypes types // only for those that have been refreshed
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -115,12 +115,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService
|
||||
// When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this.
|
||||
// Similarly, when a branch is published, next time the content is requested, the parent will be published,
|
||||
// this works because we don't cache null values.
|
||||
if (preview is false && contentCacheNode is not null)
|
||||
if (preview is false && contentCacheNode is not null && HasPublishedAncestorPath(contentCacheNode.Key) is false)
|
||||
{
|
||||
if (HasPublishedAncestorPath(contentCacheNode.Key) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// Careful not to early return here. We need to complete the scope even if returning null.
|
||||
contentCacheNode = null;
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
|
||||
+141
-141
@@ -1,32 +1,32 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"version": "15.4.0-rc",
|
||||
"version": "15.4.0-rc3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"version": "15.4.0-rc",
|
||||
"version": "15.4.0-rc3",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"./src/packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@tiptap/core": "2.11.5",
|
||||
"@tiptap/extension-character-count": "^2.11.5",
|
||||
"@tiptap/extension-image": "2.11.5",
|
||||
"@tiptap/extension-link": "2.11.5",
|
||||
"@tiptap/extension-placeholder": "2.11.5",
|
||||
"@tiptap/extension-subscript": "2.11.5",
|
||||
"@tiptap/extension-superscript": "2.11.5",
|
||||
"@tiptap/extension-table": "2.11.5",
|
||||
"@tiptap/extension-table-cell": "2.11.5",
|
||||
"@tiptap/extension-table-header": "2.11.5",
|
||||
"@tiptap/extension-table-row": "2.11.5",
|
||||
"@tiptap/extension-text-align": "2.11.5",
|
||||
"@tiptap/extension-underline": "2.11.5",
|
||||
"@tiptap/pm": "2.11.5",
|
||||
"@tiptap/starter-kit": "2.11.5",
|
||||
"@tiptap/core": "2.11.7",
|
||||
"@tiptap/extension-character-count": "2.11.7",
|
||||
"@tiptap/extension-image": "2.11.7",
|
||||
"@tiptap/extension-link": "2.11.7",
|
||||
"@tiptap/extension-placeholder": "2.11.7",
|
||||
"@tiptap/extension-subscript": "2.11.7",
|
||||
"@tiptap/extension-superscript": "2.11.7",
|
||||
"@tiptap/extension-table": "2.11.7",
|
||||
"@tiptap/extension-table-cell": "2.11.7",
|
||||
"@tiptap/extension-table-header": "2.11.7",
|
||||
"@tiptap/extension-table-row": "2.11.7",
|
||||
"@tiptap/extension-text-align": "2.11.7",
|
||||
"@tiptap/extension-underline": "2.11.7",
|
||||
"@tiptap/pm": "2.11.7",
|
||||
"@tiptap/starter-kit": "2.11.7",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@umbraco-ui/uui": "^1.13.0",
|
||||
"@umbraco-ui/uui-css": "^1.13.0",
|
||||
@@ -2534,9 +2534,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.5.tgz",
|
||||
"integrity": "sha512-jb0KTdUJaJY53JaN7ooY3XAxHQNoMYti/H6ANo707PsLXVeEqJ9o8+eBup1JU5CuwzrgnDc2dECt2WIGX9f8Jw==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.11.7.tgz",
|
||||
"integrity": "sha512-zN+NFFxLsxNEL8Qioc+DL6b8+Tt2bmRbXH22Gk6F6nD30x83eaUSFlSv3wqvgyCq3I1i1NO394So+Agmayx6rQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2547,9 +2547,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-blockquote": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.11.5.tgz",
|
||||
"integrity": "sha512-MZfcRIzKRD8/J1hkt/eYv49060GTL6qGR3NY/oTDuw2wYzbQXXLEbjk8hxAtjwNn7G+pWQv3L+PKFzZDxibLuA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.11.7.tgz",
|
||||
"integrity": "sha512-liD8kWowl3CcYCG9JQlVx1eSNc/aHlt6JpVsuWvzq6J8APWX693i3+zFqyK2eCDn0k+vW62muhSBe3u09hA3Zw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2560,9 +2560,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bold": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.11.5.tgz",
|
||||
"integrity": "sha512-OAq03MHEbl7MtYCUzGuwb0VpOPnM0k5ekMbEaRILFU5ZC7cEAQ36XmPIw1dQayrcuE8GZL35BKub2qtRxyC9iA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.11.7.tgz",
|
||||
"integrity": "sha512-VTR3JlldBixXbjpLTFme/Bxf1xeUgZZY3LTlt5JDlCW3CxO7k05CIa+kEZ8LXpog5annytZDUVtWqxrNjmsuHQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2573,9 +2573,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-bullet-list": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.5.tgz",
|
||||
"integrity": "sha512-VXwHlX6A/T6FAspnyjbKDO0TQ+oetXuat6RY1/JxbXphH42nLuBaGWJ6pgy6xMl6XY8/9oPkTNrfJw/8/eeRwA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.11.7.tgz",
|
||||
"integrity": "sha512-WbPogE2/Q3e3/QYgbT1Sj4KQUfGAJNc5pvb7GrUbvRQsAh7HhtuO8hqdDwH8dEdD/cNUehgt17TO7u8qV6qeBw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2586,9 +2586,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-character-count": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.11.5.tgz",
|
||||
"integrity": "sha512-Da2VGb7ClmKwXdQdQC2735qylYD8/MQAPA0skPEcHxcDTDuI8ibyIDnMPnczgS/hR5g0TYE2DQp/dkhJXeovkQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.11.7.tgz",
|
||||
"integrity": "sha512-gcVbKou+uxzg8N0BBKceLwtpWvN8g2TIjTuCdyAcAPukX63DqVWOkofFHn1RqZbstJmtF4pTGZs9OH/GJrp27Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2600,9 +2600,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.5.tgz",
|
||||
"integrity": "sha512-xOvHevNIQIcCCVn9tpvXa1wBp0wHN/2umbAZGTVzS+AQtM7BTo0tz8IyzwxkcZJaImONcUVYLOLzt2AgW1LltA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.7.tgz",
|
||||
"integrity": "sha512-VpPO1Uy/eF4hYOpohS/yMOcE1C07xmMj0/D989D9aS1x95jWwUVrSkwC+PlWMUBx9PbY2NRsg1ZDwVvlNKZ6yQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2613,9 +2613,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.5.tgz",
|
||||
"integrity": "sha512-ksxMMvqLDlC+ftcQLynqZMdlJT1iHYZorXsXw/n+wuRd7YElkRkd6YWUX/Pq/njFY6lDjKiqFLEXBJB8nrzzBA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.11.7.tgz",
|
||||
"integrity": "sha512-To/y/2H04VWqiANy53aXjV7S6fA86c2759RsH1hTIe57jA1KyE7I5tlAofljOLZK/covkGmPeBddSPHGJbz++Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2627,9 +2627,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.5.tgz",
|
||||
"integrity": "sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.11.7.tgz",
|
||||
"integrity": "sha512-95ouJXPjdAm9+VBRgFo4lhDoMcHovyl/awORDI8gyEn0Rdglt+ZRZYoySFzbVzer9h0cre+QdIwr9AIzFFbfdA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2640,9 +2640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-dropcursor": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.5.tgz",
|
||||
"integrity": "sha512-uIN7L3FU0904ec7FFFbndO7RQE/yiON4VzAMhNn587LFMyWO8US139HXIL4O8dpZeYwYL3d1FnDTflZl6CwLlg==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.11.7.tgz",
|
||||
"integrity": "sha512-63mL+nxQILizsr5NbmgDeOjFEWi34BLt7evwL6UUZEVM15K8V1G8pD9Y0kCXrZYpHWz0tqFRXdrhDz0Ppu8oVw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2654,9 +2654,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-gapcursor": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.5.tgz",
|
||||
"integrity": "sha512-kcWa+Xq9cb6lBdiICvLReuDtz/rLjFKHWpW3jTTF3FiP3wx4H8Rs6bzVtty7uOVTfwupxZRiKICAMEU6iT0xrQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.11.7.tgz",
|
||||
"integrity": "sha512-EceesmPG7FyjXZ8EgeJPUov9G1mAf2AwdypxBNH275g6xd5dmU/KvjoFZjmQ0X1ve7mS+wNupVlGxAEUYoveew==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2668,9 +2668,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-hard-break": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.5.tgz",
|
||||
"integrity": "sha512-q9doeN+Yg9F5QNTG8pZGYfNye3tmntOwch683v0CCVCI4ldKaLZ0jG3NbBTq+mosHYdgOH2rNbIORlRRsQ+iYQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.11.7.tgz",
|
||||
"integrity": "sha512-zTkZSA6q+F5sLOdCkiC2+RqJQN0zdsJqvFIOVFL/IDVOnq6PZO5THzwRRLvOSnJJl3edRQCl/hUgS0L5sTInGQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2681,9 +2681,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-heading": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.11.5.tgz",
|
||||
"integrity": "sha512-x/MV53psJ9baRcZ4k4WjnCUBMt8zCX7mPlKVT+9C/o+DEs/j/qxPLs95nHeQv70chZpSwCQCt93xMmuF0kPoAg==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.11.7.tgz",
|
||||
"integrity": "sha512-8kWh7y4Rd2fwxfWOhFFWncHdkDkMC1Z60yzIZWjIu72+6yQxvo8w3yeb7LI7jER4kffbMmadgcfhCHC/fkObBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2694,9 +2694,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-history": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.5.tgz",
|
||||
"integrity": "sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.11.7.tgz",
|
||||
"integrity": "sha512-Cu5x3aS13I040QSRoLdd+w09G4OCVfU+azpUqxufZxeNs9BIJC+0jowPLeOxKDh6D5GGT2A8sQtxc6a/ssbs8g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2708,9 +2708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.5.tgz",
|
||||
"integrity": "sha512-3up2r1Du8/5/4ZYzTC0DjTwhgPI3dn8jhOCLu73m5F3OGvK/9whcXoeWoX103hYMnGDxBlfOje71yQuN35FL4A==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.11.7.tgz",
|
||||
"integrity": "sha512-uVmQwD2dzZ5xwmvUlciy0ItxOdOfQjH6VLmu80zyJf8Yu7mvwP8JyxoXUX0vd1xHpwAhgQ9/ozjIWYGIw79DPQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2722,9 +2722,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-image": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.11.5.tgz",
|
||||
"integrity": "sha512-HbUq9AL8gb8eSuQfY/QKkvMc66ZFN/b6jvQAILGArNOgalUfGizoC6baKTJShaExMSPjBZlaAHtJiQKPaGRHaA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.11.7.tgz",
|
||||
"integrity": "sha512-YvCmTDB7Oo+A56tR4S/gcNaYpqU4DDlSQcRp5IQvmQV5EekSe0lnEazGDoqOCwsit9qQhj4MPQJhKrnaWrJUrg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2735,9 +2735,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.11.5.tgz",
|
||||
"integrity": "sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.11.7.tgz",
|
||||
"integrity": "sha512-r985bkQfG0HMpmCU0X0p/Xe7U1qgRm2mxvcp6iPCuts2FqxaCoyfNZ8YnMsgVK1mRhM7+CQ5SEg2NOmQNtHvPw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2748,9 +2748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-link": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.11.5.tgz",
|
||||
"integrity": "sha512-4Iu/aPzevbYpe50xDI0ZkqRa6nkZ9eF270Ue2qaF3Ab47nehj+9Jl78XXzo8+LTyFMnrETI73TAs1aC/IGySeQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.11.7.tgz",
|
||||
"integrity": "sha512-qKIowE73aAUrnQCIifYP34xXOHOsZw46cT/LBDlb0T60knVfQoKVE4ku08fJzAV+s6zqgsaaZ4HVOXkQYLoW7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"linkifyjs": "^4.2.0"
|
||||
@@ -2765,9 +2765,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-list-item": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.5.tgz",
|
||||
"integrity": "sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.11.7.tgz",
|
||||
"integrity": "sha512-6ikh7Y+qAbkSuIHXPIINqfzmWs5uIGrylihdZ9adaIyvrN1KSnWIqrZIk/NcZTg5YFIJlXrnGSRSjb/QM3WUhw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2778,9 +2778,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-ordered-list": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.5.tgz",
|
||||
"integrity": "sha512-Cu8KwruBNWAaEfshRQR0yOSaUKAeEwxW7UgbvF9cN/zZuKgK5uZosPCPTehIFCcRe+TBpRtZQh+06f/gNYpYYg==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.11.7.tgz",
|
||||
"integrity": "sha512-bLGCHDMB0vbJk7uu8bRg8vES3GsvxkX7Cgjgm/6xysHFbK98y0asDtNxkW1VvuRreNGz4tyB6vkcVCfrxl4jKw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2791,9 +2791,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-paragraph": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.5.tgz",
|
||||
"integrity": "sha512-YFBWeg7xu/sBnsDIF/+nh9Arf7R0h07VZMd0id5Ydd2Qe3c1uIZwXxeINVtH0SZozuPIQFAT8ICe9M0RxmE+TA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.11.7.tgz",
|
||||
"integrity": "sha512-Pl3B4q6DJqTvvAdraqZaNP9Hh0UWEHL5nNdxhaRNuhKaUo7lq8wbDSIxIW3lvV0lyCs0NfyunkUvSm1CXb6d4Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2804,9 +2804,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-placeholder": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.11.5.tgz",
|
||||
"integrity": "sha512-Pr+0Ju/l2ZvXMd9VQxtaoSZbs0BBp1jbBDqwms88ctpyvQFRfLSfSkqudQcSHyw2ROOz2E31p/7I7fpI8Y0CLA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.11.7.tgz",
|
||||
"integrity": "sha512-/06zXV4HIjYoiaUq1fVJo/RcU8pHbzx21evOpeG/foCfNpMI4xLU/vnxdUi6/SQqpZMY0eFutDqod1InkSOqsg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2818,9 +2818,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-strike": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.11.5.tgz",
|
||||
"integrity": "sha512-PVfUiCqrjvsLpbIoVlegSY8RlkR64F1Rr2RYmiybQfGbg+AkSZXDeO0eIrc03//4gua7D9DfIozHmAKv1KN3ow==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.11.7.tgz",
|
||||
"integrity": "sha512-D6GYiW9F24bvAY7XMOARNZbC8YGPzdzWdXd8VOOJABhf4ynMi/oW4NNiko+kZ67jn3EGaKoz32VMJzNQgYi1HA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2831,9 +2831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-subscript": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.11.5.tgz",
|
||||
"integrity": "sha512-VpaSzxku/Bcvf4SgDB2K5d0E+FNA/56iJHMygg/WXsq2F4tMMUEivQHI/n+17ndUEO4Wybz0wItnM1G2JfRuLQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.11.7.tgz",
|
||||
"integrity": "sha512-I25ZexCddFJ9701DCCtQbX3Vtxzj5d9ss2GAXVweIUCdATCScaebsznyUQoN5papmhTxXsw5OD+K2ZHxP82pew==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2844,9 +2844,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-superscript": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.11.5.tgz",
|
||||
"integrity": "sha512-sK6v2G0zFfGW+j9CmYp2e+tyZ3FTa3dP0xY4kJzefgZcHhMJLlLnjxBRwHCSi/jj5ie6WdZT4KoEooxnPs1Vzw==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.11.7.tgz",
|
||||
"integrity": "sha512-dNRpCcRJs0Qvv0sZRgbH7Y5hDVbWsGSZjtwFCs/mysPrvHqmXjzo7568kYWTggxEYxnXw6n0FfkCAEHlt0N90Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2857,9 +2857,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.11.5.tgz",
|
||||
"integrity": "sha512-NKXLhKWdAdURklm98YkCd2ai4fh8jY8HS/+X2s/2QiQt8Z98CU1keCm35fJEEExM234iB/hCqG5vY4JgTc0Tvw==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.11.7.tgz",
|
||||
"integrity": "sha512-rfwWkNXz/EZuhc8lylsCWPbx0Xr5FlIhreWFyeoXYrDEO3x4ytYcVOpNmbabJYP2semfM0PvPR5o84zfFkLZyg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2871,9 +2871,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-cell": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.11.5.tgz",
|
||||
"integrity": "sha512-S967Au0pgeULstP3FaasOf/LEh72p61Ooh1PcUMF/az4x8EeGgpcEUARpVUxsGxLFvogv6LmhPHZdtcGgdHcBw==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.11.7.tgz",
|
||||
"integrity": "sha512-JMOkSYRckc5SJP86yGGiHzCxCR8ecrRENvTWAKib6qer2tutxs5u42W+Z8uTcHC2dRz7Fv54snOkDoqPwkf6cw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2884,9 +2884,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-header": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.11.5.tgz",
|
||||
"integrity": "sha512-O1iBtzZP1XZDi4h1Xmgq1T63il+fpKPvBIMZ0JJH9TyCw5i5rcrMLL2dyy5zaWK3BFRJuYBNSke4c+VWnr/g6w==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.11.7.tgz",
|
||||
"integrity": "sha512-wPRKpliS5QQXgsp//ZjXrHMdLICMkjg2fUrQinOiBa7wDL5C7Y+SehtuK4s2tjeAkyAdj+nepfftyBRIlUSMXg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2897,9 +2897,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-row": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.11.5.tgz",
|
||||
"integrity": "sha512-+/VWhCuW24BcM5aaIc/f0bC6ZR1Q5gnuqw13MIo7gyPx7iIY6BXK8roGiZSs8wYAN4uBEf3EKFm0bSZwQuAeyg==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.11.7.tgz",
|
||||
"integrity": "sha512-K254RiXWGXGjz5Cm835hqfQiwnYXm8aw6oOa3isDh4A1B+1Ev4DB2vEDKMrgaOor3nbTsSYmAx2iEMrZSbpaRg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2910,9 +2910,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.5.tgz",
|
||||
"integrity": "sha512-Gq1WwyhFpCbEDrLPIHt5A8aLSlf8bfz4jm417c8F/JyU0J5dtYdmx0RAxjnLw1i7ZHE7LRyqqAoS0sl7JHDNSQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.11.7.tgz",
|
||||
"integrity": "sha512-wObCn8qZkIFnXTLvBP+X8KgaEvTap/FJ/i4hBMfHBCKPGDx99KiJU6VIbDXG8d5ZcFZE0tOetK1pP5oI7qgMlQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2923,9 +2923,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-align": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.11.5.tgz",
|
||||
"integrity": "sha512-Ei0zDpH5N9EV59ogydK4HTKa4lCPicCsQllM5n/Nf2tUJPir3aiYxzJ73FzhComD4Hpo1ANYnmssBhy8QeoPZA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.11.7.tgz",
|
||||
"integrity": "sha512-3M8zd9ROADXazVNpgR6Ejs1evSvBveN36qN4GgV71GqrNlTcjqYgQcXFLQrsd2hnE+aXir8/8bLJ+aaJXDninA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2936,9 +2936,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text-style": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.11.5.tgz",
|
||||
"integrity": "sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.11.7.tgz",
|
||||
"integrity": "sha512-LHO6DBg/9SkCQFdWlVfw9nolUmw+Cid94WkTY+7IwrpyG2+ZGQxnKpCJCKyeaFNbDoYAtvu0vuTsSXeCkgShcA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2949,9 +2949,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-underline": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.11.5.tgz",
|
||||
"integrity": "sha512-YpWHXNIkSoRSuzT2cvgKpyJ2tTz3LzqkTM64uC+uTJ8cUkvXIWUWejJR42q8ma/mTlQe4lHff4IQ0Sf58Digtw==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.11.7.tgz",
|
||||
"integrity": "sha512-NtoQw6PGijOAtXC6G+0Aq0/Z5wwEjPhNHs8nsjXogfWIgaj/aI4/zfBnA06eI3WT+emMYQTl0fTc4CUPnLVU8g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -2962,9 +2962,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.5.tgz",
|
||||
"integrity": "sha512-z9JFtqc5ZOsdQLd9vRnXfTCQ8v5ADAfRt9Nm7SqP6FUHII8E1hs38ACzf5xursmth/VonJYb5+73Pqxk1hGIPw==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.11.7.tgz",
|
||||
"integrity": "sha512-7gEEfz2Q6bYKXM07vzLUD0vqXFhC5geWRA6LCozTiLdVFDdHWiBrvb2rtkL5T7mfLq03zc1QhH7rI3F6VntOEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.2.1",
|
||||
@@ -2981,7 +2981,7 @@
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.4.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.6.3",
|
||||
"prosemirror-tables": "^1.6.4",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.37.0"
|
||||
@@ -2992,32 +2992,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/starter-kit": {
|
||||
"version": "2.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.11.5.tgz",
|
||||
"integrity": "sha512-SLI7Aj2ruU1t//6Mk8f+fqW+18uTqpdfLUJYgwu0CkqBckrkRZYZh6GVLk/02k3H2ki7QkFxiFbZrdbZdng0JA==",
|
||||
"version": "2.11.7",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.11.7.tgz",
|
||||
"integrity": "sha512-K+q51KwNU/l0kqRuV5e1824yOLVftj6kGplGQLvJG56P7Rb2dPbM/JeaDbxQhnHT/KDGamG0s0Po0M3pPY163A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tiptap/core": "^2.11.5",
|
||||
"@tiptap/extension-blockquote": "^2.11.5",
|
||||
"@tiptap/extension-bold": "^2.11.5",
|
||||
"@tiptap/extension-bullet-list": "^2.11.5",
|
||||
"@tiptap/extension-code": "^2.11.5",
|
||||
"@tiptap/extension-code-block": "^2.11.5",
|
||||
"@tiptap/extension-document": "^2.11.5",
|
||||
"@tiptap/extension-dropcursor": "^2.11.5",
|
||||
"@tiptap/extension-gapcursor": "^2.11.5",
|
||||
"@tiptap/extension-hard-break": "^2.11.5",
|
||||
"@tiptap/extension-heading": "^2.11.5",
|
||||
"@tiptap/extension-history": "^2.11.5",
|
||||
"@tiptap/extension-horizontal-rule": "^2.11.5",
|
||||
"@tiptap/extension-italic": "^2.11.5",
|
||||
"@tiptap/extension-list-item": "^2.11.5",
|
||||
"@tiptap/extension-ordered-list": "^2.11.5",
|
||||
"@tiptap/extension-paragraph": "^2.11.5",
|
||||
"@tiptap/extension-strike": "^2.11.5",
|
||||
"@tiptap/extension-text": "^2.11.5",
|
||||
"@tiptap/extension-text-style": "^2.11.5",
|
||||
"@tiptap/pm": "^2.11.5"
|
||||
"@tiptap/core": "^2.11.7",
|
||||
"@tiptap/extension-blockquote": "^2.11.7",
|
||||
"@tiptap/extension-bold": "^2.11.7",
|
||||
"@tiptap/extension-bullet-list": "^2.11.7",
|
||||
"@tiptap/extension-code": "^2.11.7",
|
||||
"@tiptap/extension-code-block": "^2.11.7",
|
||||
"@tiptap/extension-document": "^2.11.7",
|
||||
"@tiptap/extension-dropcursor": "^2.11.7",
|
||||
"@tiptap/extension-gapcursor": "^2.11.7",
|
||||
"@tiptap/extension-hard-break": "^2.11.7",
|
||||
"@tiptap/extension-heading": "^2.11.7",
|
||||
"@tiptap/extension-history": "^2.11.7",
|
||||
"@tiptap/extension-horizontal-rule": "^2.11.7",
|
||||
"@tiptap/extension-italic": "^2.11.7",
|
||||
"@tiptap/extension-list-item": "^2.11.7",
|
||||
"@tiptap/extension-ordered-list": "^2.11.7",
|
||||
"@tiptap/extension-paragraph": "^2.11.7",
|
||||
"@tiptap/extension-strike": "^2.11.7",
|
||||
"@tiptap/extension-text": "^2.11.7",
|
||||
"@tiptap/extension-text-style": "^2.11.7",
|
||||
"@tiptap/pm": "^2.11.7"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@umbraco-cms/backoffice",
|
||||
"license": "MIT",
|
||||
"version": "15.4.0-rc",
|
||||
"version": "15.4.4",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": null,
|
||||
@@ -202,21 +202,21 @@
|
||||
"npm": ">=10.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tiptap/core": "2.11.5",
|
||||
"@tiptap/extension-character-count": "^2.11.5",
|
||||
"@tiptap/extension-image": "2.11.5",
|
||||
"@tiptap/extension-link": "2.11.5",
|
||||
"@tiptap/extension-placeholder": "2.11.5",
|
||||
"@tiptap/extension-subscript": "2.11.5",
|
||||
"@tiptap/extension-superscript": "2.11.5",
|
||||
"@tiptap/extension-table": "2.11.5",
|
||||
"@tiptap/extension-table-cell": "2.11.5",
|
||||
"@tiptap/extension-table-header": "2.11.5",
|
||||
"@tiptap/extension-table-row": "2.11.5",
|
||||
"@tiptap/extension-text-align": "2.11.5",
|
||||
"@tiptap/extension-underline": "2.11.5",
|
||||
"@tiptap/pm": "2.11.5",
|
||||
"@tiptap/starter-kit": "2.11.5",
|
||||
"@tiptap/core": "2.11.7",
|
||||
"@tiptap/extension-character-count": "2.11.7",
|
||||
"@tiptap/extension-image": "2.11.7",
|
||||
"@tiptap/extension-link": "2.11.7",
|
||||
"@tiptap/extension-placeholder": "2.11.7",
|
||||
"@tiptap/extension-subscript": "2.11.7",
|
||||
"@tiptap/extension-superscript": "2.11.7",
|
||||
"@tiptap/extension-table": "2.11.7",
|
||||
"@tiptap/extension-table-cell": "2.11.7",
|
||||
"@tiptap/extension-table-header": "2.11.7",
|
||||
"@tiptap/extension-table-row": "2.11.7",
|
||||
"@tiptap/extension-text-align": "2.11.7",
|
||||
"@tiptap/extension-underline": "2.11.7",
|
||||
"@tiptap/pm": "2.11.7",
|
||||
"@tiptap/starter-kit": "2.11.7",
|
||||
"@types/diff": "^7.0.1",
|
||||
"@umbraco-ui/uui": "^1.13.0",
|
||||
"@umbraco-ui/uui-css": "^1.13.0",
|
||||
|
||||
@@ -2097,7 +2097,7 @@ export default {
|
||||
duplicateUsername: "Username '%0%' is already taken",
|
||||
customValidation: 'Custom validation',
|
||||
entriesShort: 'Minimum %0% entries, requires <strong>%1%</strong> more.',
|
||||
entriesExceed: 'Maximum %0% entries, <strong>%1%</strong> too many.',
|
||||
entriesExceed: 'Maximum %0% entries, you have entered <strong>%1%</strong> too many.',
|
||||
entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.',
|
||||
},
|
||||
healthcheck: {
|
||||
|
||||
@@ -2168,7 +2168,7 @@ export default {
|
||||
invalidPattern: 'Value is invalid, it does not match the correct pattern',
|
||||
customValidation: 'Custom validation',
|
||||
entriesShort: 'Minimum %0% entries, requires <strong>%1%</strong> more.',
|
||||
entriesExceed: 'Maximum %0% entries, <strong>%1%</strong> too many.',
|
||||
entriesExceed: 'Maximum %0% entries, you have entered <strong>%1%</strong> too many.',
|
||||
entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.',
|
||||
invalidMemberGroupName: 'Invalid member group name',
|
||||
invalidUserGroupName: 'Invalid user group name',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3523,6 +3523,12 @@ export type PutDocumentBlueprintByIdMoveData = {
|
||||
|
||||
export type PutDocumentBlueprintByIdMoveResponse = (string);
|
||||
|
||||
export type GetDocumentBlueprintByIdScaffoldData = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type GetDocumentBlueprintByIdScaffoldResponse = ((DocumentBlueprintResponseModel));
|
||||
|
||||
export type PostDocumentBlueprintFolderData = {
|
||||
requestBody?: (CreateFolderRequestModel);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './validation.context.js';
|
||||
export * from './validation.context-token.js';
|
||||
export * from './validation-messages.manager.js';
|
||||
export * from './server-model-validator.context-token.js';
|
||||
export * from './server-model-validator.context.js';
|
||||
|
||||
+14
-1
@@ -4,10 +4,23 @@ import { UMB_DOCUMENT_BLUEPRINT_DETAIL_STORE_CONTEXT } from './document-blueprin
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbDetailRepositoryBase } from '@umbraco-cms/backoffice/repository';
|
||||
|
||||
export class UmbDocumentBlueprintDetailRepository extends UmbDetailRepositoryBase<UmbDocumentBlueprintDetailModel> {
|
||||
export class UmbDocumentBlueprintDetailRepository extends UmbDetailRepositoryBase<
|
||||
UmbDocumentBlueprintDetailModel,
|
||||
UmbDocumentBlueprintServerDataSource
|
||||
> {
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, UmbDocumentBlueprintServerDataSource, UMB_DOCUMENT_BLUEPRINT_DETAIL_STORE_CONTEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an existing document blueprint by its unique identifier for scaffolding purposes, i.e. to create a new document based on an existing blueprint.
|
||||
* @param {string} unique - The unique identifier of the document blueprint.
|
||||
* @returns {UmbRepositoryResponse<UmbDocumentBlueprintDetailModel>} - The document blueprint data.
|
||||
* @memberof UmbDocumentBlueprintDetailRepository
|
||||
*/
|
||||
scaffoldByUnique(unique: string) {
|
||||
return this.detailDataSource.scaffoldByUnique(unique);
|
||||
}
|
||||
}
|
||||
|
||||
export { UmbDocumentBlueprintDetailRepository as api };
|
||||
|
||||
+52
-31
@@ -1,9 +1,10 @@
|
||||
import type { UmbDocumentBlueprintDetailModel } from '../../types.js';
|
||||
import { UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE } from '../../entity.js';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
|
||||
import type { UmbDataSourceResponse, UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
|
||||
import type {
|
||||
CreateDocumentBlueprintRequestModel,
|
||||
DocumentBlueprintResponseModel,
|
||||
UpdateDocumentBlueprintRequestModel,
|
||||
} from '@umbraco-cms/backoffice/external/backend-api';
|
||||
import { DocumentBlueprintService } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
@@ -74,7 +75,7 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
|
||||
* @returns {*}
|
||||
* @memberof UmbDocumentBlueprintServerDataSource
|
||||
*/
|
||||
async read(unique: string) {
|
||||
async read(unique: string): Promise<UmbDataSourceResponse<UmbDocumentBlueprintDetailModel>> {
|
||||
if (!unique) throw new Error('Unique is missing');
|
||||
|
||||
const { data, error } = await tryExecuteAndNotify(
|
||||
@@ -86,35 +87,24 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
|
||||
return { error };
|
||||
}
|
||||
|
||||
// TODO: make data mapper to prevent errors
|
||||
const document: UmbDocumentBlueprintDetailModel = {
|
||||
entityType: UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
|
||||
unique: data.id,
|
||||
values: data.values.map((value) => {
|
||||
return {
|
||||
editorAlias: value.editorAlias,
|
||||
culture: value.culture || null,
|
||||
segment: value.segment || null,
|
||||
alias: value.alias,
|
||||
value: value.value,
|
||||
};
|
||||
}),
|
||||
variants: data.variants.map((variant) => {
|
||||
return {
|
||||
state: variant.state,
|
||||
culture: variant.culture || null,
|
||||
segment: variant.segment || null,
|
||||
name: variant.name,
|
||||
publishDate: variant.publishDate || null,
|
||||
createDate: variant.createDate,
|
||||
updateDate: variant.updateDate,
|
||||
};
|
||||
}),
|
||||
documentType: {
|
||||
unique: data.documentType.id,
|
||||
collection: data.documentType.collection ? { unique: data.documentType.collection.id } : null,
|
||||
},
|
||||
};
|
||||
const document = this.#createDocumentBlueprintDetailModel(data);
|
||||
|
||||
return { data: document };
|
||||
}
|
||||
|
||||
async scaffoldByUnique(unique: string): Promise<UmbDataSourceResponse<UmbDocumentBlueprintDetailModel>> {
|
||||
if (!unique) throw new Error('Unique is missing');
|
||||
|
||||
const { data, error } = await tryExecuteAndNotify(
|
||||
this.#host,
|
||||
DocumentBlueprintService.getDocumentBlueprintByIdScaffold({ id: unique }),
|
||||
);
|
||||
|
||||
if (error || !data) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
const document = this.#createDocumentBlueprintDetailModel(data);
|
||||
|
||||
return { data: document };
|
||||
}
|
||||
@@ -196,4 +186,35 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource
|
||||
// TODO: update to delete when implemented
|
||||
return tryExecuteAndNotify(this.#host, DocumentBlueprintService.deleteDocumentBlueprintById({ id: unique }));
|
||||
}
|
||||
|
||||
#createDocumentBlueprintDetailModel(data: DocumentBlueprintResponseModel): UmbDocumentBlueprintDetailModel {
|
||||
return {
|
||||
entityType: UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
|
||||
unique: data.id,
|
||||
values: data.values.map((value) => {
|
||||
return {
|
||||
editorAlias: value.editorAlias,
|
||||
culture: value.culture || null,
|
||||
segment: value.segment || null,
|
||||
alias: value.alias,
|
||||
value: value.value,
|
||||
};
|
||||
}),
|
||||
variants: data.variants.map((variant) => {
|
||||
return {
|
||||
state: variant.state,
|
||||
culture: variant.culture || null,
|
||||
segment: variant.segment || null,
|
||||
name: variant.name,
|
||||
publishDate: variant.publishDate || null,
|
||||
createDate: variant.createDate,
|
||||
updateDate: variant.updateDate,
|
||||
};
|
||||
}),
|
||||
documentType: {
|
||||
unique: data.documentType.id,
|
||||
collection: data.documentType.collection ? { unique: data.documentType.collection.id } : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -48,7 +48,10 @@ export class UmbDocumentDuplicateToModalElement extends UmbModalBaseElement<
|
||||
<uui-box id="tree-box" headline="Duplicate to">
|
||||
<umb-tree
|
||||
alias=${UMB_DOCUMENT_TREE_ALIAS}
|
||||
.props=${{ expandTreeRoot: true }}
|
||||
.props=${{
|
||||
expandTreeRoot: true,
|
||||
hideTreeItemActions: true,
|
||||
}}
|
||||
@selection-change=${this.#onTreeSelectionChange}></umb-tree>
|
||||
</uui-box>
|
||||
<uui-box headline="Options">
|
||||
|
||||
+6
-4
@@ -211,14 +211,16 @@ export class UmbDocumentWorkspaceContext
|
||||
async create(parent: UmbEntityModel, documentTypeUnique: string, blueprintUnique?: string) {
|
||||
if (blueprintUnique) {
|
||||
const blueprintRepository = new UmbDocumentBlueprintDetailRepository(this);
|
||||
const { data } = await blueprintRepository.requestByUnique(blueprintUnique);
|
||||
const { data } = await blueprintRepository.scaffoldByUnique(blueprintUnique);
|
||||
|
||||
if (!data) throw new Error('Blueprint data is missing');
|
||||
|
||||
return this.createScaffold({
|
||||
parent,
|
||||
preset: {
|
||||
documentType: data?.documentType,
|
||||
values: data?.values,
|
||||
variants: data?.variants as Array<UmbDocumentVariantModel>,
|
||||
documentType: data.documentType,
|
||||
values: data.values,
|
||||
variants: data.variants as Array<UmbDocumentVariantModel>,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+6
-1
@@ -78,7 +78,12 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement {
|
||||
|
||||
get source(): string {
|
||||
if (this.src) {
|
||||
return `${this._serverUrl}${this.src}`;
|
||||
// Test that URL is relative:
|
||||
if (this.src.startsWith('/')) {
|
||||
return `${this._serverUrl}${this.src}`;
|
||||
} else {
|
||||
return this.src;
|
||||
}
|
||||
}
|
||||
|
||||
return this.fileDataUrl ?? '';
|
||||
|
||||
+3
@@ -461,6 +461,7 @@ export function getMimeTypeFromExtension(extension: string): string | null {
|
||||
'.onetoc2': 'application/onenote',
|
||||
'.opf': 'application/oebps-package+xml',
|
||||
'.oprc': 'application/vnd.palm',
|
||||
'.opus': 'audio/ogg',
|
||||
'.org': 'application/vnd.lotus-organizer',
|
||||
'.osf': 'application/vnd.yamaha.openscoreformat',
|
||||
'.osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml',
|
||||
@@ -744,6 +745,8 @@ export function getMimeTypeFromExtension(extension: string): string | null {
|
||||
'.wbxml': 'application/vnd.wap.wbxml',
|
||||
'.wcm': 'application/vnd.ms-works',
|
||||
'.wdb': 'application/vnd.ms-works',
|
||||
'.weba': 'audio/webm',
|
||||
'.webm': 'video/webm',
|
||||
'.webp': 'image/webp',
|
||||
'.wiz': 'application/msword',
|
||||
'.wks': 'application/vnd.ms-works',
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type
|
||||
import { html } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { Meta } from '@storybook/web-components';
|
||||
|
||||
import './property-editor-ui-tiny-mce-stylesheets-configuration.element.js';
|
||||
import './property-editor-ui-stylesheet-picker.element.js';
|
||||
import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type';
|
||||
|
||||
const dataTypeData = umbDataTypeMockDb.read('dt-richTextEditor') as unknown as UmbDataTypeDetailModel;
|
||||
@@ -10,7 +10,7 @@ const dataTypeData = umbDataTypeMockDb.read('dt-richTextEditor') as unknown as U
|
||||
export default {
|
||||
title: 'Property Editor UIs/Stylesheet Picker',
|
||||
component: 'umb-property-editor-ui-stylesheet-picker',
|
||||
id: 'umb-property-editor-ui-sstylesheet-picker',
|
||||
id: 'umb-property-editor-ui-stylesheet-picker',
|
||||
} as Meta;
|
||||
|
||||
export const AAAOverview = ({ value }: any) =>
|
||||
|
||||
+28
-3
@@ -1,6 +1,15 @@
|
||||
import type { UmbTiptapExtensionApi } from '../../extensions/types.js';
|
||||
import type { UmbTiptapStatusbarValue, UmbTiptapToolbarValue } from '../types.js';
|
||||
import { css, customElement, html, map, property, state, unsafeCSS, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import {
|
||||
css,
|
||||
customElement,
|
||||
html,
|
||||
property,
|
||||
repeat,
|
||||
state,
|
||||
unsafeCSS,
|
||||
when,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { Editor } from '@umbraco-cms/backoffice/external/tiptap';
|
||||
@@ -16,6 +25,12 @@ import './tiptap-statusbar.element.js';
|
||||
|
||||
const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials';
|
||||
|
||||
/**
|
||||
* The root path for the stylesheets on the server.
|
||||
* This is used to load the stylesheets from the server as a workaround until the server supports virtual paths.
|
||||
*/
|
||||
const STYLESHEET_ROOT_PATH = '/css';
|
||||
|
||||
@customElement('umb-input-tiptap')
|
||||
export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof UmbLitElement, string>(UmbLitElement) {
|
||||
#stylesheets = new Set(['/umbraco/backoffice/css/rte-content.css']);
|
||||
@@ -129,7 +144,13 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
|
||||
|
||||
const stylesheets = this.configuration?.getValueByAlias<Array<string>>('stylesheets');
|
||||
if (stylesheets?.length) {
|
||||
stylesheets.forEach((x) => this.#stylesheets.add(x));
|
||||
stylesheets.forEach((stylesheet) => {
|
||||
const linkHref =
|
||||
stylesheet.startsWith('http') || stylesheet.startsWith(STYLESHEET_ROOT_PATH)
|
||||
? stylesheet
|
||||
: `${STYLESHEET_ROOT_PATH}${stylesheet}`;
|
||||
this.#stylesheets.add(linkHref);
|
||||
});
|
||||
}
|
||||
|
||||
this._toolbar = this.configuration?.getValueByAlias<UmbTiptapToolbarValue>('toolbar') ?? [[[]]];
|
||||
@@ -182,7 +203,11 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
|
||||
#renderStyles() {
|
||||
if (!this._styles?.length) return;
|
||||
return html`
|
||||
${map(this.#stylesheets, (stylesheet) => html`<link rel="stylesheet" href=${stylesheet} />`)}
|
||||
${repeat(
|
||||
this.#stylesheets,
|
||||
(stylesheet) => stylesheet,
|
||||
(stylesheet) => html`<link rel="stylesheet" href="${stylesheet}" />`,
|
||||
)}
|
||||
<style>
|
||||
${this._styles.map((style) => unsafeCSS(style))}
|
||||
</style>
|
||||
|
||||
@@ -189,25 +189,48 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
|
||||
|
||||
private void ExecuteBuilderAttributes(IUmbracoBuilder builder)
|
||||
{
|
||||
// todo better errors
|
||||
Type? testClassType = GetTestClassType()
|
||||
?? throw new Exception($"Could not find test class for {TestContext.CurrentContext.Test.FullName} in order to execute builder attributes.");
|
||||
|
||||
// execute builder attributes defined on method
|
||||
foreach (ConfigureBuilderAttribute builderAttribute in Type.GetType(TestContext.CurrentContext.Test.ClassName)
|
||||
.GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName)
|
||||
.GetCustomAttributes(typeof(ConfigureBuilderAttribute), true))
|
||||
// Execute builder attributes defined on method.
|
||||
foreach (ConfigureBuilderAttribute builderAttribute in GetConfigureBuilderAttributes<ConfigureBuilderAttribute>(testClassType))
|
||||
{
|
||||
builderAttribute.Execute(builder);
|
||||
}
|
||||
|
||||
// execute builder attributes defined on method with param value passtrough from testcase
|
||||
foreach (ConfigureBuilderTestCaseAttribute builderAttribute in Type.GetType(TestContext.CurrentContext.Test.ClassName)
|
||||
.GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName)
|
||||
.GetCustomAttributes(typeof(ConfigureBuilderTestCaseAttribute), true))
|
||||
// Execute builder attributes defined on method with param value pass through from test case.
|
||||
foreach (ConfigureBuilderTestCaseAttribute builderAttribute in GetConfigureBuilderAttributes<ConfigureBuilderTestCaseAttribute>(testClassType))
|
||||
{
|
||||
builderAttribute.Execute(builder);
|
||||
}
|
||||
}
|
||||
|
||||
private static Type? GetTestClassType()
|
||||
{
|
||||
string testClassName = TestContext.CurrentContext.Test.ClassName;
|
||||
|
||||
// Try resolving the type name directly (which will work for tests in this assembly).
|
||||
Type testClass = Type.GetType(testClassName);
|
||||
if (testClass is not null)
|
||||
{
|
||||
return testClass;
|
||||
}
|
||||
|
||||
// Try scanning the loaded assemblies to see if we can find the class by full name. This will be necessary
|
||||
// for integration test projects using the base classess provided by Umbraco.
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
return assemblies
|
||||
.SelectMany(a => a.GetTypes().Where(t => t.FullName == testClassName))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private static IEnumerable<TAttribute> GetConfigureBuilderAttributes<TAttribute>(Type testClassType)
|
||||
where TAttribute : Attribute =>
|
||||
testClassType
|
||||
.GetMethods().First(m => m.Name == TestContext.CurrentContext.Test.MethodName)
|
||||
.GetCustomAttributes(typeof(TAttribute), true)
|
||||
.Cast<TAttribute>();
|
||||
|
||||
/// <summary>
|
||||
/// Hook for altering UmbracoBuilder setup
|
||||
/// </summary>
|
||||
|
||||
@@ -57,7 +57,7 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054
|
||||
Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Id);
|
||||
Subpage.Key = new Guid(SubPageKey);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(Subpage, -1, contentSchedule);
|
||||
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Cache;
|
||||
|
||||
// We need to make sure that it's the distributed cache refreshers that refresh the elements cache
|
||||
// see: https://github.com/umbraco/Umbraco-CMS/issues/18467
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
|
||||
internal sealed class DistributedCacheRefresherTests : UmbracoIntegrationTest
|
||||
{
|
||||
private IElementsCache ElementsCache => GetRequiredService<IElementsCache>();
|
||||
|
||||
private ContentCacheRefresher ContentCacheRefresher => GetRequiredService<ContentCacheRefresher>();
|
||||
|
||||
private MediaCacheRefresher MediaCacheRefresher => GetRequiredService<MediaCacheRefresher>();
|
||||
|
||||
[Test]
|
||||
public void DistributedContentCacheRefresherClearsElementsCache()
|
||||
{
|
||||
var cacheKey = "test";
|
||||
PopulateCache("test");
|
||||
|
||||
ContentCacheRefresher.Refresh([new ContentCacheRefresher.JsonPayload()]);
|
||||
|
||||
Assert.IsNull(ElementsCache.Get(cacheKey));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DistributedMediaCacheRefresherClearsElementsCache()
|
||||
{
|
||||
var cacheKey = "test";
|
||||
PopulateCache("test");
|
||||
|
||||
MediaCacheRefresher.Refresh([new MediaCacheRefresher.JsonPayload(1, Guid.NewGuid(), TreeChangeTypes.RefreshAll)]);
|
||||
|
||||
Assert.IsNull(ElementsCache.Get(cacheKey));
|
||||
}
|
||||
|
||||
private void PopulateCache(string key)
|
||||
{
|
||||
ElementsCache.Get(key, () => new object());
|
||||
|
||||
// Just making sure something is in the cache now.
|
||||
Assert.IsNotNull(ElementsCache.Get(key));
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
ctVariant.Variations = ContentVariation.Culture;
|
||||
ContentTypeService.Save(ctVariant);
|
||||
|
||||
var now = DateTime.Now;
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// 10x invariant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds
|
||||
var invariant = new List<IContent>();
|
||||
@@ -321,7 +321,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
// Act
|
||||
var content = ContentService.CreateAndSave("Test", Constants.System.Root, "umbTextpage");
|
||||
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddHours(2));
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddHours(2));
|
||||
ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule);
|
||||
Assert.AreEqual(1, contentSchedule.FullSchedule.Count);
|
||||
|
||||
@@ -676,7 +676,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
var root = ContentService.GetById(Textpage.Id);
|
||||
ContentService.Publish(root!, root!.AvailableCultures.ToArray());
|
||||
var content = ContentService.GetById(Subpage.Id);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddSeconds(1));
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddSeconds(1));
|
||||
ContentService.PersistContentSchedule(content!, contentSchedule);
|
||||
ContentService.Publish(content, content.AvailableCultures.ToArray());
|
||||
|
||||
@@ -1386,7 +1386,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
// Arrange
|
||||
var content = ContentService.GetById(Subpage.Id); // This Content expired 5min ago
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddMinutes(-5));
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddMinutes(-5));
|
||||
ContentService.Save(content, contentSchedule: contentSchedule);
|
||||
|
||||
var parent = ContentService.GetById(Textpage.Id);
|
||||
@@ -1416,7 +1416,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
|
||||
var content = ContentBuilder.CreateBasicContent(contentType);
|
||||
content.SetCultureName("Hello", "en-US");
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.Now.AddMinutes(-5));
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.UtcNow.AddMinutes(-5));
|
||||
ContentService.Save(content, contentSchedule: contentSchedule);
|
||||
|
||||
var published = ContentService.Publish(content, new[] { "en-US" });
|
||||
@@ -1431,7 +1431,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
// Arrange
|
||||
var content = ContentService.GetById(Subpage.Id);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null);
|
||||
ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule);
|
||||
|
||||
var parent = ContentService.GetById(Textpage.Id);
|
||||
@@ -1488,7 +1488,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
content.Properties[0].SetValue("Foo", string.Empty);
|
||||
contentService.Save(content);
|
||||
contentService.PersistContentSchedule(content,
|
||||
ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null));
|
||||
ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null));
|
||||
|
||||
// Act
|
||||
var result = contentService.Publish(content, Array.Empty<string>(), userId: Constants.Security.SuperUserId);
|
||||
@@ -1540,7 +1540,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
contentService.Publish(content, Array.Empty<string>());
|
||||
|
||||
contentService.PersistContentSchedule(content,
|
||||
ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null));
|
||||
ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null));
|
||||
contentService.Save(content);
|
||||
|
||||
// Act
|
||||
@@ -1568,7 +1568,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent
|
||||
|
||||
var content = ContentBuilder.CreateBasicContent(contentType);
|
||||
content.SetCultureName("Hello", "en-US");
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.Now.AddHours(2), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddHours(2), null);
|
||||
ContentService.Save(content, contentSchedule: contentSchedule);
|
||||
|
||||
var published = ContentService.Publish(content, new[] { "en-US" });
|
||||
|
||||
@@ -230,7 +230,7 @@ public class DocumentUrlServiceTests : UmbracoIntegrationTestWithContent
|
||||
// Create a subpage
|
||||
var subsubpage = ContentBuilder.CreateSimpleContent(ContentType, documentName, Subpage.Id);
|
||||
subsubpage.Key = Guid.Parse(documentKey);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(subsubpage, -1, contentSchedule);
|
||||
|
||||
if (loadDraft is false)
|
||||
@@ -248,7 +248,7 @@ public class DocumentUrlServiceTests : UmbracoIntegrationTestWithContent
|
||||
// Create a second root
|
||||
var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null);
|
||||
secondRoot.Key = new Guid("8E21BCD4-02CA-483D-84B0-1FC92702E198");
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(secondRoot, -1, contentSchedule);
|
||||
|
||||
if (loadDraft is false)
|
||||
@@ -266,7 +266,7 @@ public class DocumentUrlServiceTests : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
// Create a second root
|
||||
var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(secondRoot, -1, contentSchedule);
|
||||
|
||||
// Create a child of second root
|
||||
|
||||
+3
-3
@@ -62,7 +62,7 @@ public class DocumentUrlServiceTests_HideTopLevel_False : UmbracoIntegrationTest
|
||||
// Create a subpage
|
||||
var subsubpage = ContentBuilder.CreateSimpleContent(ContentType, "Sub Page 1", Subpage.Id);
|
||||
subsubpage.Key = new Guid("DF49F477-12F2-4E33-8563-91A7CC1DCDBB");
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(subsubpage, -1, contentSchedule);
|
||||
|
||||
if (loadDraft is false)
|
||||
@@ -81,7 +81,7 @@ public class DocumentUrlServiceTests_HideTopLevel_False : UmbracoIntegrationTest
|
||||
// Create a second root
|
||||
var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null);
|
||||
secondRoot.Key = new Guid("8E21BCD4-02CA-483D-84B0-1FC92702E198");
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(secondRoot, -1, contentSchedule);
|
||||
|
||||
if (loadDraft is false)
|
||||
@@ -100,7 +100,7 @@ public class DocumentUrlServiceTests_HideTopLevel_False : UmbracoIntegrationTest
|
||||
{
|
||||
// Create a second root
|
||||
var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(secondRoot, -1, contentSchedule);
|
||||
|
||||
// Create a child of second root
|
||||
|
||||
@@ -164,7 +164,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent
|
||||
{
|
||||
var grandchild = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild", Subpage2.Id);
|
||||
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(grandchild, -1, contentSchedule);
|
||||
|
||||
var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]);
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ public class PublishedUrlInfoProviderTests : PublishedUrlInfoProviderTestsBase
|
||||
{
|
||||
// Create a second root
|
||||
var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(secondRoot, -1, contentSchedule);
|
||||
|
||||
// Create a child of second root
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public class PublishedUrlInfoProvider_hidetoplevel_false : PublishedUrlInfoProvi
|
||||
{
|
||||
// Create a second root
|
||||
var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(secondRoot, -1, contentSchedule);
|
||||
|
||||
// Create a child of second root
|
||||
|
||||
+609
-21
@@ -14,6 +14,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
|
||||
|
||||
internal partial class BlockListElementLevelVariationTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests whether the user can update the variant values of existing blocks inside an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
[ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))]
|
||||
@@ -169,6 +173,10 @@ internal partial class BlockListElementLevelVariationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can add new variant blocks to an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
[ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))]
|
||||
@@ -299,6 +307,10 @@ internal partial class BlockListElementLevelVariationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can update the variant values of existing blocks inside an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Handle_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess)
|
||||
@@ -457,6 +469,10 @@ internal partial class BlockListElementLevelVariationTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can add new variant blocks to an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Handle_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault_WithoutInitialValue(bool updateWithLimitedUserAccess)
|
||||
@@ -524,14 +540,14 @@ internal partial class BlockListElementLevelVariationTests
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
|
||||
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) },
|
||||
},
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
|
||||
}
|
||||
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] },
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
|
||||
@@ -539,27 +555,12 @@ internal partial class BlockListElementLevelVariationTests
|
||||
|
||||
content = ContentService.GetById(content.Key);
|
||||
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
|
||||
Assert.NotNull(savedBlocksValue);
|
||||
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
|
||||
blockListValue = savedBlocksValue is null ? null : JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
|
||||
|
||||
// the Danish values should be updated regardless of the executing user
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual("#1: The second content value in Danish", blockListValue.ContentData[0].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
Assert.AreEqual("#1: The second settings value in Danish", blockListValue.SettingsData[0].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
|
||||
Assert.AreEqual("#2: The second content value in Danish", blockListValue.ContentData[1].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
Assert.AreEqual("#2: The second settings value in Danish", blockListValue.SettingsData[1].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
});
|
||||
|
||||
// limited user access means invariant, English and German should not have been updated - changes should be rolled back to the initial block values
|
||||
// limited user access means invariant data is inaccessible since AllowEditInvariantFromNonDefault is disabled
|
||||
if (updateWithLimitedUserAccess)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(1, blockListValue.ContentData[0].Values.Count);
|
||||
Assert.AreEqual(1, blockListValue.ContentData[1].Values.Count);
|
||||
});
|
||||
Assert.IsNull(blockListValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -581,6 +582,542 @@ internal partial class BlockListElementLevelVariationTests
|
||||
Assert.AreEqual("#2: The second settings value in English", blockListValue.SettingsData[1].Values[1].Value);
|
||||
Assert.AreEqual("#2: The second content value in German", blockListValue.ContentData[1].Values[3].Value);
|
||||
Assert.AreEqual("#2: The second settings value in German", blockListValue.SettingsData[1].Values[3].Value);
|
||||
|
||||
Assert.AreEqual("#1: The second content value in Danish", blockListValue.ContentData[0].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
Assert.AreEqual("#1: The second settings value in Danish", blockListValue.SettingsData[0].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
|
||||
Assert.AreEqual("#2: The second content value in Danish", blockListValue.ContentData[1].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
Assert.AreEqual("#2: The second settings value in Danish", blockListValue.SettingsData[1].Values.Single(v => v.Culture == "da-DK").Value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can add/remove new variant blocks to an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Handle_BlockStructureManipulation_For_Limited_Users_Without_AllowEditInvariantFromNonDefault(
|
||||
bool updateWithLimitedUserAccess)
|
||||
{
|
||||
await LanguageService.CreateAsync(
|
||||
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
|
||||
var userKey = updateWithLimitedUserAccess
|
||||
? (await CreateLimitedUser()).Key
|
||||
: Constants.Security.SuperUserKey;
|
||||
|
||||
var elementType = CreateElementType(ContentVariation.Culture);
|
||||
var blockListDataType = await CreateBlockListDataType(elementType);
|
||||
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
|
||||
var content = CreateContent(contentType, elementType, [], false);
|
||||
content.SetCultureName("Home (de)", "de-DE");
|
||||
ContentService.Save(content);
|
||||
|
||||
var firstContentElementKey = Guid.NewGuid();
|
||||
var firstSettingsElementKey = Guid.NewGuid();
|
||||
|
||||
var secondContentElementKey = Guid.NewGuid();
|
||||
var secondSettingsElementKey = Guid.NewGuid();
|
||||
|
||||
var blockListValue = BlockListPropertyValue(
|
||||
elementType,
|
||||
[
|
||||
(
|
||||
firstContentElementKey,
|
||||
firstSettingsElementKey,
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" },
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
null,
|
||||
null)),
|
||||
(
|
||||
secondContentElementKey,
|
||||
secondSettingsElementKey,
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" },
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
null,
|
||||
null))
|
||||
]);
|
||||
|
||||
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
|
||||
ContentService.Save(content);
|
||||
|
||||
var newContentElementKey = Guid.NewGuid();
|
||||
RemoveBlock(blockListValue, firstContentElementKey);
|
||||
AddBlock(
|
||||
blockListValue,
|
||||
new BlockItemData
|
||||
{
|
||||
Key = newContentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values = new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#new: The new invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#new: The new settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#new: The new settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#new: The new settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
},
|
||||
null,
|
||||
elementType);
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
|
||||
},
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
content = ContentService.GetById(content.Key);
|
||||
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
|
||||
Assert.NotNull(savedBlocksValue);
|
||||
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
|
||||
|
||||
if (updateWithLimitedUserAccess)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// new one can't be added
|
||||
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == newContentElementKey));
|
||||
Assert.AreEqual(0, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
|
||||
Assert.AreEqual(0, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
|
||||
|
||||
// can't remove first
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
|
||||
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
|
||||
// second wasn't touched
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
|
||||
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// add new one, did not add settings
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == newContentElementKey));
|
||||
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
|
||||
Assert.AreEqual(0, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
|
||||
|
||||
// first one removed
|
||||
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
|
||||
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
|
||||
Assert.AreEqual(0, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
Assert.AreEqual(0, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
|
||||
// second wasn't touched
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
|
||||
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can add/remove new variant blocks to an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
[ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))]
|
||||
public async Task Can_Handle_BlockStructureManipulation_For_Limited_Users_With_AllowEditInvariantFromNonDefault(
|
||||
bool updateWithLimitedUserAccess)
|
||||
{
|
||||
await LanguageService.CreateAsync(
|
||||
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
|
||||
var userKey = updateWithLimitedUserAccess
|
||||
? (await CreateLimitedUser()).Key
|
||||
: Constants.Security.SuperUserKey;
|
||||
|
||||
var elementType = CreateElementType(ContentVariation.Culture);
|
||||
var blockListDataType = await CreateBlockListDataType(elementType);
|
||||
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
|
||||
var content = CreateContent(contentType, elementType, [], false);
|
||||
content.SetCultureName("Home (de)", "de-DE");
|
||||
ContentService.Save(content);
|
||||
|
||||
var firstContentElementKey = Guid.NewGuid();
|
||||
var firstSettingsElementKey = Guid.NewGuid();
|
||||
|
||||
var blockListValue = BlockListPropertyValue(
|
||||
elementType,
|
||||
[
|
||||
(
|
||||
firstContentElementKey,
|
||||
firstSettingsElementKey,
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" },
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
null,
|
||||
null)),
|
||||
(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" },
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
null,
|
||||
null))
|
||||
]);
|
||||
|
||||
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
|
||||
ContentService.Save(content);
|
||||
|
||||
var newContentElementKey = Guid.NewGuid();
|
||||
RemoveBlock(blockListValue, firstContentElementKey);
|
||||
AddBlock(
|
||||
blockListValue,
|
||||
new BlockItemData
|
||||
{
|
||||
Key = newContentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values = new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#new: The new invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#new: The new settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#new: The new settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#new: The new settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
},
|
||||
null,
|
||||
elementType);
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
|
||||
},
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
content = ContentService.GetById(content.Key);
|
||||
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
|
||||
Assert.NotNull(savedBlocksValue);
|
||||
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
|
||||
|
||||
// In both cases we are allowed to change the invariant structure
|
||||
// But the amount of new cultured values we can add differs
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == newContentElementKey));
|
||||
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
|
||||
Assert.AreEqual(0, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
|
||||
Assert.AreEqual(updateWithLimitedUserAccess ? 2 : 4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#new") == true)));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can update the variant values of existing blocks inside an invariant blocklist
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_ClearBlocks_Limited_User_Access_To_Languages_Without_AllowEditInvariantFromNonDefault(bool updateWithLimitedUserAccess)
|
||||
{
|
||||
await LanguageService.CreateAsync(
|
||||
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
|
||||
var userKey = updateWithLimitedUserAccess
|
||||
? (await CreateLimitedUser()).Key
|
||||
: Constants.Security.SuperUserKey;
|
||||
|
||||
var elementType = CreateElementType(ContentVariation.Culture);
|
||||
var blockListDataType = await CreateBlockListDataType(elementType);
|
||||
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
|
||||
var content = CreateContent(contentType, elementType, [], false);
|
||||
content.SetCultureName("Home (de)", "de-DE");
|
||||
ContentService.Save(content);
|
||||
|
||||
var blockListValue = BlockListPropertyValue(
|
||||
elementType,
|
||||
[
|
||||
(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" }
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" }
|
||||
},
|
||||
null,
|
||||
null
|
||||
)
|
||||
),
|
||||
(
|
||||
Guid.NewGuid(),
|
||||
Guid.NewGuid(),
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" }
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" }
|
||||
},
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
var serializedBlockListValue = JsonSerializer.Serialize(blockListValue);
|
||||
content.Properties["blocks"]!.SetValue(serializedBlockListValue);
|
||||
ContentService.Save(content);
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "blocks", Value = null },
|
||||
},
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] },
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
content = ContentService.GetById(content.Key);
|
||||
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
|
||||
|
||||
// limited user access means English and German should not have been updated - changes should be rolled back to the initial block values
|
||||
if (updateWithLimitedUserAccess)
|
||||
{
|
||||
Assert.NotNull(savedBlocksValue);
|
||||
Assert.AreEqual(serializedBlockListValue, savedBlocksValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNull(savedBlocksValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the user can add/remove a value for a given culture
|
||||
/// </summary>
|
||||
/// <param name="updateWithLimitedUserAccess">true => danish only which is not the default. false => admin which is all languages</param>
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task Can_Handle_ValueRemoval_For_Limited_Users(
|
||||
bool updateWithLimitedUserAccess)
|
||||
{
|
||||
await LanguageService.CreateAsync(
|
||||
new Language("de-DE", "German"), Constants.Security.SuperUserKey);
|
||||
var userKey = updateWithLimitedUserAccess
|
||||
? (await CreateLimitedUser()).Key
|
||||
: Constants.Security.SuperUserKey;
|
||||
|
||||
var elementType = CreateElementType(ContentVariation.Culture);
|
||||
var blockListDataType = await CreateBlockListDataType(elementType);
|
||||
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
|
||||
var content = CreateContent(contentType, elementType, [], false);
|
||||
content.SetCultureName("Home (de)", "de-DE");
|
||||
ContentService.Save(content);
|
||||
|
||||
var firstContentElementKey = Guid.NewGuid();
|
||||
var firstSettingsElementKey = Guid.NewGuid();
|
||||
|
||||
var secondContentElementKey = Guid.NewGuid();
|
||||
var secondSettingsElementKey = Guid.NewGuid();
|
||||
|
||||
var blockListValue = BlockListPropertyValue(
|
||||
elementType,
|
||||
[
|
||||
(
|
||||
firstContentElementKey,
|
||||
firstSettingsElementKey,
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first content value in German", Culture = "de-DE" },
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#1: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#1: The first settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
null,
|
||||
null)),
|
||||
(
|
||||
secondContentElementKey,
|
||||
secondSettingsElementKey,
|
||||
new BlockProperty(
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant content value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first content value in German", Culture = "de-DE" },
|
||||
},
|
||||
new List<BlockPropertyValue> {
|
||||
new() { Alias = "invariantText", Value = "#2: The first invariant settings value" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" },
|
||||
new() { Alias = "variantText", Value = "#2: The first settings value in German", Culture = "de-DE" },
|
||||
},
|
||||
null,
|
||||
null))
|
||||
]);
|
||||
|
||||
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
|
||||
ContentService.Save(content);
|
||||
|
||||
// remove a value the limited user can remove
|
||||
blockListValue.ContentData.First().Values.RemoveAll(value => value.Culture == "da-DK");
|
||||
blockListValue.SettingsData.First().Values.RemoveAll(value => value.Culture == "da-DK");
|
||||
// remove a value the admin user can remove
|
||||
blockListValue.ContentData.First().Values.RemoveAll(value => value.Culture == "en-US");
|
||||
blockListValue.SettingsData.First().Values.RemoveAll(value => value.Culture == "en-US");
|
||||
|
||||
var updateModel = new ContentUpdateModel
|
||||
{
|
||||
InvariantProperties = new[]
|
||||
{
|
||||
new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) }
|
||||
},
|
||||
Variants = new[]
|
||||
{
|
||||
new VariantModel { Name = content.GetCultureName("en-US")!, Culture = "en-US", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("da-DK")!, Culture = "da-DK", Properties = [] },
|
||||
new VariantModel { Name = content.GetCultureName("de-DE")!, Culture = "de-DE", Properties = [] }
|
||||
},
|
||||
};
|
||||
|
||||
var result = await ContentEditingService.UpdateAsync(content.Key, updateModel, userKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
|
||||
content = ContentService.GetById(content.Key);
|
||||
var savedBlocksValue = content?.Properties["blocks"]?.GetValue()?.ToString();
|
||||
Assert.NotNull(savedBlocksValue);
|
||||
blockListValue = JsonSerializer.Deserialize<BlockListValue>(savedBlocksValue);
|
||||
|
||||
if (updateWithLimitedUserAccess)
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
|
||||
// Should only have removed the danish value
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
|
||||
Assert.AreEqual(3, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
Assert.AreEqual(3, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
Assert.AreEqual(0, blockListValue.ContentData.First().Values.Count(value => value.Culture == "da-DK"));
|
||||
Assert.AreEqual(1, blockListValue.ContentData.First().Values.Count(value => value.Culture == "en-US"));
|
||||
Assert.AreEqual(0, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "da-DK"));
|
||||
Assert.AreEqual(1, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "en-US"));
|
||||
|
||||
// second wasn't touched
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
|
||||
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
// both danish and english should be removed
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == firstContentElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == firstSettingsElementKey));
|
||||
Assert.AreEqual(2, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
Assert.AreEqual(2, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#1") == true)));
|
||||
Assert.AreEqual(0, blockListValue.ContentData.First().Values.Count(value => value.Culture == "da-DK"));
|
||||
Assert.AreEqual(0, blockListValue.ContentData.First().Values.Count(value => value.Culture == "en-US"));
|
||||
Assert.AreEqual(0, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "da-DK"));
|
||||
Assert.AreEqual(0, blockListValue.SettingsData.First().Values.Count(value => value.Culture == "en-US"));
|
||||
|
||||
// second wasn't touched
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.SettingsKey == secondSettingsElementKey));
|
||||
Assert.AreEqual(1, blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Count(layoutItem => layoutItem.ContentKey == secondContentElementKey));
|
||||
Assert.AreEqual(4, blockListValue.ContentData.Sum(contentData => contentData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
Assert.AreEqual(4, blockListValue.SettingsData.Sum(settingsData => settingsData.Values.Count(value => value.Value?.ToString()?.StartsWith("#2") == true)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -940,4 +1477,55 @@ internal partial class BlockListElementLevelVariationTests
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private void AddBlock(BlockListValue listValue, BlockItemData contentData, BlockItemData? settingsData, IContentType elementType)
|
||||
{
|
||||
listValue.ContentData.Add(contentData);
|
||||
if (settingsData != null)
|
||||
{
|
||||
listValue.SettingsData.Add(settingsData);
|
||||
}
|
||||
|
||||
var cultures = elementType.VariesByCulture()
|
||||
? contentData.Values.Select(value => value.Culture)
|
||||
.WhereNotNull()
|
||||
.Distinct()
|
||||
.ToArray()
|
||||
: [null];
|
||||
if (cultures.Any() is false)
|
||||
{
|
||||
cultures = [null];
|
||||
}
|
||||
|
||||
var segments = elementType.VariesBySegment()
|
||||
? contentData.Values.Select(value => value.Segment)
|
||||
.Distinct()
|
||||
.ToArray()
|
||||
: [null];
|
||||
|
||||
foreach (var exposeItem in cultures.SelectMany(culture => segments.Select(segment =>
|
||||
new BlockItemVariation(contentData.Key, culture, segment))))
|
||||
{
|
||||
listValue.Expose.Add(exposeItem);
|
||||
}
|
||||
|
||||
|
||||
listValue.Layout[Constants.PropertyEditors.Aliases.BlockList] = listValue
|
||||
.Layout[Constants.PropertyEditors.Aliases.BlockList]
|
||||
.Append(new BlockListLayoutItem { ContentKey = contentData.Key, SettingsKey = settingsData?.Key });
|
||||
}
|
||||
|
||||
private void RemoveBlock(BlockListValue listValue, Guid blockKey)
|
||||
{
|
||||
// remove the item from the layout
|
||||
var layoutItem = listValue.Layout[Constants.PropertyEditors.Aliases.BlockList].First(x => x.ContentKey == blockKey);
|
||||
listValue.Layout[Constants.PropertyEditors.Aliases.BlockList] = listValue.Layout[Constants.PropertyEditors.Aliases.BlockList].Where(layout => layout.ContentKey != blockKey);
|
||||
listValue.ContentData.RemoveAll(contentData => contentData.Key == blockKey);
|
||||
if (layoutItem.SettingsKey != null)
|
||||
{
|
||||
listValue.SettingsData.RemoveAll(settingsData => settingsData.Key == layoutItem.SettingsKey);
|
||||
}
|
||||
|
||||
listValue.Expose.RemoveAll(exposeItem => exposeItem.ContentKey == blockKey);
|
||||
}
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
# Block Element Level Variant
|
||||
|
||||
### Notes
|
||||
- When talking about variant data, we mean language variant
|
||||
- Segment variants are not taken into account at this moment but can be expected to work in a similar manner
|
||||
|
||||
### What is it
|
||||
When an element document type supports variant data (marked as variant and has a property marked as variant) is used
|
||||
inside an invariant property on a variant document type, that property is considered to have element level variant data
|
||||
which is a form of partial variant data.
|
||||
|
||||
When a property editor supports partial variant data (`IDataEditor.CanMergePartialPropertyValues()`) the
|
||||
`ContentEditingService` will run the `IDataEditor.MergeVariantInvariantPropertyValue(...)` method to get a valid
|
||||
value according to the rules defined for that propertyEditor
|
||||
|
||||
Block Element level variant data is this within all (core) property editors derived from blocks
|
||||
- Umbraco.BlockList
|
||||
- Umbraco.BlockGrid
|
||||
- Umbraco.RichText
|
||||
|
||||
Most logic regarding this feature can be found in `BlockValuePropertyValueEditorBase`
|
||||
|
||||
### Axioms
|
||||
1. A `null` value for a property, including element level variation based properties, is a valid value
|
||||
2. The invariant value holds the structure/representation of the underlying variant values
|
||||
3. The structure takes precedence over the underlying data
|
||||
|
||||
## Editing Data
|
||||
|
||||
### Access to invariant data
|
||||
- All Languages: The user has access to all languages
|
||||
- Default Language: The user has access to the language that is defined as the default
|
||||
- AllowEditInvariantFromNonDefault: Configuration setting
|
||||
|
||||
| All Languages | Default Language | AllowEditInvariantFromNonDefault | Can Edit Invariant |
|
||||
|---------------|------------------|----------------------------------|--------------------|
|
||||
| True | Inherits True | N/A | True |
|
||||
| False | True | N/A | True |
|
||||
| False | False | True | True |
|
||||
| False | False | False | False |
|
||||
|
||||
|
||||
### Rules derived from the axioms
|
||||
- A user with access to invariant data is allowed to add or remove blocks even if those blocks hold language variant
|
||||
data they do not have access to.
|
||||
- A user without access to invariant data is NOT allowed to add or remove blocks.
|
||||
- A user can only edit element variant properties for the languages they have access to.
|
||||
- A user is allowed to clear (set value to `null`) an element level variation as long as they have access to edit invariant data.
|
||||
|
||||
## Exposing
|
||||
When a block is defined on invariant level but a language has not had its variant fields filled in yet,
|
||||
the variant version of the block might be empty or considered not ready for publishing. The Expose feature allows
|
||||
editors to define in which culture a block is ready to be consumed by the publishing process.
|
||||
|
||||
The client currently adds a blocks culture to the expose list when editing for that blocks starts in the culture,
|
||||
more precisely when inline editor for the block is opened.
|
||||
|
||||
From an API perspective you are allowed to add and remove cultures from the expose list as long as you have the permissions to do so
|
||||
|
||||
### Axioms
|
||||
- Expose data is linked to the same permissions as variant editing
|
||||
- Variant blocks that are not exposed for a specific culture should not be processed by the publish feature
|
||||
|
||||
### Rules derived from the axioms
|
||||
- Only a user with access to a language should be able to remove or add a block to the expose list for that language
|
||||
- A block that is not exposed for a given language should not exist in the published value of the document for that language
|
||||
- A block that is not exposed should not be processed when running validation during publishing or by running the validation separately.
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Tests.Integration.Attributes;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
|
||||
|
||||
public partial class ContentBlueprintEditingServiceTests
|
||||
{
|
||||
public static void AddScaffoldedNotificationHandler(IUmbracoBuilder builder)
|
||||
=> builder.AddNotificationHandler<ContentScaffoldedNotification, ContentScaffoldedNotificationHandler>();
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
[ConfigureBuilder(ActionName = nameof(AddScaffoldedNotificationHandler))]
|
||||
public async Task Can_Get_Scaffold(bool variant)
|
||||
{
|
||||
var blueprint = await (variant ? CreateVariantContentBlueprint() : CreateInvariantContentBlueprint());
|
||||
try
|
||||
{
|
||||
ContentScaffoldedNotificationHandler.ContentScaffolded = notification =>
|
||||
{
|
||||
foreach (var propertyValue in notification.Scaffold.Properties.SelectMany(property => property.Values))
|
||||
{
|
||||
propertyValue.EditedValue += " scaffolded edited";
|
||||
propertyValue.PublishedValue += " scaffolded published";
|
||||
}
|
||||
};
|
||||
var result = await ContentBlueprintEditingService.GetScaffoldedAsync(blueprint.Key);
|
||||
Assert.IsNotNull(result);
|
||||
Assert.AreEqual(blueprint.Key, result.Key);
|
||||
|
||||
var propertyValues = result.Properties.SelectMany(property => property.Values).ToArray();
|
||||
Assert.IsNotEmpty(propertyValues);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(propertyValues.All(value => value.EditedValue is string stringValue && stringValue.EndsWith(" scaffolded edited")));
|
||||
Assert.IsTrue(propertyValues.All(value => value.PublishedValue is string stringValue && stringValue.EndsWith(" scaffolded published")));
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
ContentScaffoldedNotificationHandler.ContentScaffolded = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Cannot_Get_Non_Existing_Scaffold()
|
||||
{
|
||||
var result = await ContentBlueprintEditingService.GetScaffoldedAsync(Guid.NewGuid());
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
public class ContentScaffoldedNotificationHandler : INotificationHandler<ContentScaffoldedNotification>
|
||||
{
|
||||
public static Action<ContentScaffoldedNotification>? ContentScaffolded { get; set; }
|
||||
|
||||
public void Handle(ContentScaffoldedNotification notification) => ContentScaffolded?.Invoke(notification);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -14,7 +14,7 @@ public partial class ContentPublishingServiceTests
|
||||
|
||||
var result = await ContentPublishingService.PublishAsync(
|
||||
Textpage.Key,
|
||||
MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.Now.AddDays(1), null)),
|
||||
MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.UtcNow.AddDays(1), null)),
|
||||
Constants.Security.SuperUserKey);
|
||||
|
||||
Assert.IsTrue(result.Success);
|
||||
@@ -25,7 +25,7 @@ public partial class ContentPublishingServiceTests
|
||||
[Test]
|
||||
public async Task Publish_Single_Item_Does_Not_Publish_Children_In_The_Future()
|
||||
{
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.Now.AddDays(1), null)), Constants.Security.SuperUserKey);
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.UtcNow.AddDays(1), null)), Constants.Security.SuperUserKey);
|
||||
|
||||
VerifyIsNotPublished(Textpage.Key);
|
||||
VerifyIsNotPublished(Subpage.Key);
|
||||
@@ -36,7 +36,7 @@ public partial class ContentPublishingServiceTests
|
||||
{
|
||||
await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(_allCultures), Constants.Security.SuperUserKey);
|
||||
|
||||
var result = await ContentPublishingService.PublishAsync(Subpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.Now.AddDays(1), null)), Constants.Security.SuperUserKey);
|
||||
var result = await ContentPublishingService.PublishAsync(Subpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.UtcNow.AddDays(1), null)), Constants.Security.SuperUserKey);
|
||||
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Status);
|
||||
|
||||
+1
-1
@@ -968,7 +968,7 @@ public class EntityServiceTests : UmbracoIntegrationTest
|
||||
|
||||
// Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054
|
||||
_subpage = ContentBuilder.CreateSimpleContent(_contentType, "Text Page 1", _textpage.Id);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
|
||||
var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null);
|
||||
ContentService.Save(_subpage, -1, contentSchedule);
|
||||
|
||||
// Create and Save Content "Text Page 2" based on "umbTextpage" -> 1055
|
||||
|
||||
@@ -286,5 +286,8 @@
|
||||
<Compile Update="ManagementApi\Services\UserStartNodeEntitiesServiceMediaTests.RootUserAccessEntities.cs">
|
||||
<DependentUpon>UserStartNodeEntitiesServiceMediaTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Infrastructure\Services\ContentBlueprintEditingServiceTests.GetScaffold.cs">
|
||||
<DependentUpon>ContentBlueprintEditingServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration;
|
||||
|
||||
[TestFixture]
|
||||
public class ContentSettingsExtensionsTests
|
||||
{
|
||||
[TestCase("jpg")]
|
||||
[TestCase("JPG")]
|
||||
[TestCase("jpg ")]
|
||||
public void IsFileAllowedForUpload_Allows_File_In_Allow_List(string extension)
|
||||
{
|
||||
var contentSettings = new ContentSettings
|
||||
{
|
||||
AllowedUploadedFileExtensions = ["jpg", "png"],
|
||||
};
|
||||
|
||||
Assert.IsTrue(contentSettings.IsFileAllowedForUpload(extension));
|
||||
}
|
||||
|
||||
[TestCase("gif")]
|
||||
[TestCase("GIF")]
|
||||
[TestCase("gif ")]
|
||||
public void IsFileAllowedForUpload_Rejects_File_Not_In_Allow_List(string extension)
|
||||
{
|
||||
var contentSettings = new ContentSettings
|
||||
{
|
||||
AllowedUploadedFileExtensions = ["jpg", "png"],
|
||||
};
|
||||
|
||||
Assert.IsFalse(contentSettings.IsFileAllowedForUpload(extension));
|
||||
}
|
||||
|
||||
[TestCase("jpg")]
|
||||
[TestCase("JPG")]
|
||||
[TestCase("jpg ")]
|
||||
public void IsFileAllowedForUpload_Allows_File_Not_In_Disallow_List(string extension)
|
||||
{
|
||||
var contentSettings = new ContentSettings
|
||||
{
|
||||
DisallowedUploadedFileExtensions = ["gif", "png"],
|
||||
};
|
||||
|
||||
Assert.IsTrue(contentSettings.IsFileAllowedForUpload(extension));
|
||||
}
|
||||
|
||||
[TestCase("gif")]
|
||||
[TestCase("GIF")]
|
||||
[TestCase("gif ")]
|
||||
public void IsFileAllowedForUpload_Rejects_File_In_Disallow_List(string extension)
|
||||
{
|
||||
var contentSettings = new ContentSettings
|
||||
{
|
||||
DisallowedUploadedFileExtensions = ["gif", "png"],
|
||||
};
|
||||
|
||||
Assert.IsFalse(contentSettings.IsFileAllowedForUpload(extension));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsFileAllowedForUpload_Allows_File_In_Allow_List_Even_If_Also_In_Disallow_List()
|
||||
{
|
||||
var contentSettings = new ContentSettings
|
||||
{
|
||||
AllowedUploadedFileExtensions = ["jpg", "png"],
|
||||
DisallowedUploadedFileExtensions = ["jpg"],
|
||||
};
|
||||
|
||||
Assert.IsTrue(contentSettings.IsFileAllowedForUpload("jpg"));
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
|
||||
|
||||
public partial class ContentExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void GetStatus_WhenTrashed_ReturnsTrashed()
|
||||
{
|
||||
var contentMock = new Mock<IContent>();
|
||||
contentMock.SetupGet(c => c.Trashed).Returns(true);
|
||||
var result = contentMock.Object.GetStatus(new ContentScheduleCollection());
|
||||
Assert.AreEqual(ContentStatus.Trashed, result);
|
||||
}
|
||||
|
||||
[TestCase(true, ContentStatus.Published)]
|
||||
[TestCase(false, ContentStatus.Unpublished)]
|
||||
public void GetStatus_WithEmptySchedule_ReturnsPublishState(bool published, ContentStatus expectedStatus)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
mock.SetupGet(c => c.Published).Returns(published);
|
||||
|
||||
var result = mock.Object.GetStatus(new ContentScheduleCollection());
|
||||
Assert.AreEqual(expectedStatus, result);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(10)]
|
||||
[TestCase(60)]
|
||||
[TestCase(120)]
|
||||
[TestCase(1000)]
|
||||
public void GetStatus_WithPendingExpiry_ForInvariant_ReturnsExpired(int minutesFromExpiry)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
|
||||
var schedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddMinutes(-1 * minutesFromExpiry));
|
||||
var result = mock.Object.GetStatus(schedule);
|
||||
Assert.AreEqual(ContentStatus.Expired, result);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(10)]
|
||||
[TestCase(60)]
|
||||
[TestCase(120)]
|
||||
[TestCase(1000)]
|
||||
public void GetStatus_WithPendingRelease_ForInvariant_ReturnsAwaitingRelease(int minutesUntilRelease)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
|
||||
var schedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(minutesUntilRelease), null);
|
||||
var result = mock.Object.GetStatus(schedule);
|
||||
Assert.AreEqual(ContentStatus.AwaitingRelease, result);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(10)]
|
||||
[TestCase(60)]
|
||||
[TestCase(120)]
|
||||
[TestCase(1000)]
|
||||
public void GetStatus_WithPastReleaseAndFutureExpiry_ForInvariant_ReturnsPublishedState(int minutes)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
mock.SetupGet(c => c.Published).Returns(true);
|
||||
|
||||
var schedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-1 * minutes), DateTime.UtcNow.AddMinutes(minutes));
|
||||
var result = mock.Object.GetStatus(schedule);
|
||||
Assert.AreEqual(ContentStatus.Published, result);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(10)]
|
||||
[TestCase(60)]
|
||||
[TestCase(120)]
|
||||
[TestCase(1000)]
|
||||
public void GetStatus_WithPendingExpiry_ForVariant_ReturnsExpired(int minutesFromExpiry)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
|
||||
var schedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.UtcNow.AddMinutes(-1 * minutesFromExpiry));
|
||||
var result = mock.Object.GetStatus(schedule, "en-US");
|
||||
Assert.AreEqual(ContentStatus.Expired, result);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(10)]
|
||||
[TestCase(60)]
|
||||
[TestCase(120)]
|
||||
[TestCase(1000)]
|
||||
public void GetStatus_WithPendingRelease_ForVariant_ReturnsAwaitingRelease(int minutesUntilRelease)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
|
||||
var schedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddMinutes(minutesUntilRelease), null);
|
||||
var result = mock.Object.GetStatus(schedule, "en-US");
|
||||
Assert.AreEqual(ContentStatus.AwaitingRelease, result);
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(10)]
|
||||
[TestCase(60)]
|
||||
[TestCase(120)]
|
||||
[TestCase(1000)]
|
||||
public void GetStatus_WithPastReleaseAndFutureExpiry_ForVariant_ReturnsPublishedState(int minutes)
|
||||
{
|
||||
var contentTypeMock = new Mock<ISimpleContentType>();
|
||||
contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
|
||||
var mock = new Mock<IContent>();
|
||||
mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object);
|
||||
mock.SetupGet(c => c.Published).Returns(true);
|
||||
|
||||
var schedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddMinutes(-1 * minutes), DateTime.UtcNow.AddMinutes(minutes));
|
||||
var result = mock.Object.GetStatus(schedule, "en-US");
|
||||
Assert.AreEqual(ContentStatus.Published, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
|
||||
|
||||
[TestFixture]
|
||||
public partial class ContentExtensionsTests
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
public class DocumentUrlServiceTests
|
||||
{
|
||||
[Test]
|
||||
public void ConvertToCacheModel_Converts_Single_Document_With_Single_Segment_To_Expected_Cache_Model()
|
||||
{
|
||||
var segments = new List<PublishedDocumentUrlSegment>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DocumentKey = Guid.NewGuid(),
|
||||
IsDraft = false,
|
||||
IsPrimary = true,
|
||||
LanguageId = 1,
|
||||
UrlSegment = "test-segment",
|
||||
},
|
||||
};
|
||||
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
|
||||
|
||||
Assert.AreEqual(1, cacheModels.Count);
|
||||
Assert.AreEqual(segments[0].DocumentKey, cacheModels[0].DocumentKey);
|
||||
Assert.AreEqual(1, cacheModels[0].LanguageId);
|
||||
Assert.AreEqual(1, cacheModels[0].UrlSegments.Count);
|
||||
Assert.AreEqual("test-segment", cacheModels[0].UrlSegments[0].Segment);
|
||||
Assert.IsTrue(cacheModels[0].UrlSegments[0].IsPrimary);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConvertToCacheModel_Converts_Multiple_Documents_With_Single_Segment_To_Expected_Cache_Model()
|
||||
{
|
||||
var segments = new List<PublishedDocumentUrlSegment>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DocumentKey = Guid.NewGuid(),
|
||||
IsDraft = false,
|
||||
IsPrimary = true,
|
||||
LanguageId = 1,
|
||||
UrlSegment = "test-segment",
|
||||
},
|
||||
new()
|
||||
{
|
||||
DocumentKey = Guid.NewGuid(),
|
||||
IsDraft = false,
|
||||
IsPrimary = true,
|
||||
LanguageId = 1,
|
||||
UrlSegment = "test-segment-2",
|
||||
},
|
||||
};
|
||||
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
|
||||
|
||||
Assert.AreEqual(2, cacheModels.Count);
|
||||
Assert.AreEqual(segments[0].DocumentKey, cacheModels[0].DocumentKey);
|
||||
Assert.AreEqual(segments[1].DocumentKey, cacheModels[1].DocumentKey);
|
||||
Assert.AreEqual(1, cacheModels[0].LanguageId);
|
||||
Assert.AreEqual(1, cacheModels[1].LanguageId);
|
||||
Assert.AreEqual(1, cacheModels[0].UrlSegments.Count);
|
||||
Assert.AreEqual("test-segment", cacheModels[0].UrlSegments[0].Segment);
|
||||
Assert.AreEqual(1, cacheModels[1].UrlSegments.Count);
|
||||
Assert.AreEqual("test-segment-2", cacheModels[1].UrlSegments[0].Segment);
|
||||
Assert.IsTrue(cacheModels[0].UrlSegments[0].IsPrimary);
|
||||
Assert.IsTrue(cacheModels[1].UrlSegments[0].IsPrimary);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConvertToCacheModel_Converts_Single_Document_With_Multiple_Segments_To_Expected_Cache_Model()
|
||||
{
|
||||
var documentKey = Guid.NewGuid();
|
||||
var segments = new List<PublishedDocumentUrlSegment>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DocumentKey = documentKey,
|
||||
IsDraft = false,
|
||||
IsPrimary = true,
|
||||
LanguageId = 1,
|
||||
UrlSegment = "test-segment",
|
||||
},
|
||||
new()
|
||||
{
|
||||
DocumentKey = documentKey,
|
||||
IsDraft = false,
|
||||
IsPrimary = false,
|
||||
LanguageId = 1,
|
||||
UrlSegment = "test-segment-2",
|
||||
},
|
||||
};
|
||||
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
|
||||
|
||||
Assert.AreEqual(1, cacheModels.Count);
|
||||
Assert.AreEqual(documentKey, cacheModels[0].DocumentKey);
|
||||
Assert.AreEqual(1, cacheModels[0].LanguageId);
|
||||
Assert.AreEqual(2, cacheModels[0].UrlSegments.Count);
|
||||
Assert.AreEqual("test-segment", cacheModels[0].UrlSegments[0].Segment);
|
||||
Assert.AreEqual("test-segment-2", cacheModels[0].UrlSegments[1].Segment);
|
||||
Assert.IsTrue(cacheModels[0].UrlSegments[0].IsPrimary);
|
||||
Assert.IsFalse(cacheModels[0].UrlSegments[1].IsPrimary);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ConvertToCacheModel_Performance_Test()
|
||||
{
|
||||
const int NumberOfSegments = 1;
|
||||
var segments = Enumerable.Range(0, NumberOfSegments)
|
||||
.Select((x, i) => new PublishedDocumentUrlSegment
|
||||
{
|
||||
DocumentKey = Guid.NewGuid(),
|
||||
IsDraft = false,
|
||||
IsPrimary = true,
|
||||
LanguageId = 1,
|
||||
UrlSegment = $"test-segment-{x + 1}",
|
||||
});
|
||||
var cacheModels = DocumentUrlService.ConvertToCacheModel(segments).ToList();
|
||||
|
||||
Assert.AreEqual(NumberOfSegments, cacheModels.Count);
|
||||
|
||||
// Benchmarking (for NumberOfSegments = 50000):
|
||||
// - Initial implementation (15.4): ~28s
|
||||
// - Current implementation: ~100ms
|
||||
}
|
||||
}
|
||||
@@ -46,5 +46,8 @@
|
||||
<Compile Update="Umbraco.Cms.Api.Management\Services\BackOfficeExternalLoginServiceTests.UnLinkLoginAsync.cs">
|
||||
<DependentUpon>BackOfficeExternalLoginServiceTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Umbraco.Core\Extensions\ContentExtensionsTests.GetStatus.cs">
|
||||
<DependentUpon>ContentExtensionsTests.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "15.4.0-rc",
|
||||
"version": "15.4.4",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user