Merge remote-tracking branch 'origin/release/16.0' into v16/dev

This commit is contained in:
mole
2025-04-30 11:12:35 +02:00
22 changed files with 286 additions and 91 deletions

View File

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

View File

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

View File

@@ -1,15 +1,35 @@
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.DeliveryApi;
public sealed class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApiContentBuilder
{
public ApiContentBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
private readonly IVariationContextAccessor _variationContextAccessor;
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
public ApiContentBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: this(
apiContentNameProvider,
apiContentRouteBuilder,
outputExpansionStrategyAccessor,
StaticServiceProvider.Instance.CreateInstance<IVariationContextAccessor>())
{
}
public ApiContentBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
=> _variationContextAccessor = variationContextAccessor;
protected override IApiContent Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties);
=> new ApiContent(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties);
}

View File

@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Extensions;
@@ -7,15 +8,36 @@ namespace Umbraco.Cms.Core.DeliveryApi;
public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
{
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
private readonly IVariationContextAccessor _variationContextAccessor;
public ApiContentResponseBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
[Obsolete("Please use the constructor that takes an IVariationContextAccessor instead. Scheduled for removal in V17.")]
public ApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
: this(
apiContentNameProvider,
apiContentRouteBuilder,
outputExpansionStrategyAccessor,
StaticServiceProvider.Instance.CreateInstance<IVariationContextAccessor>())
{
}
public ApiContentResponseBuilder(
IApiContentNameProvider apiContentNameProvider,
IApiContentRouteBuilder apiContentRouteBuilder,
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor,
IVariationContextAccessor variationContextAccessor)
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
=> _apiContentRouteBuilder = apiContentRouteBuilder;
{
_apiContentRouteBuilder = apiContentRouteBuilder;
_variationContextAccessor = variationContextAccessor;
}
protected override IApiContentResponse Create(IPublishedContent content, string name, IApiContentRoute route, IDictionary<string, object?> properties)
{
IDictionary<string, IApiContentRoute> cultures = GetCultures(content);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.UpdateDate, route, properties, cultures);
return new ApiContentResponse(content.Key, name, content.ContentType.Alias, content.CreateDate, content.CultureDate(_variationContextAccessor), route, properties, cultures);
}
protected virtual IDictionary<string, IApiContentRoute> GetCultures(IPublishedContent content)

View File

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

View File

@@ -2457,6 +2457,7 @@ export default {
labelForCopyToClipboard: 'Copy to clipboard',
confirmDeleteHeadline: 'Delete from clipboard',
confirmDeleteDescription: 'Are you sure you want to delete <strong>{0}</strong> from the clipboard?',
copySuccessHeadline: 'Copied to clipboard',
},
propertyActions: {
tooltipForPropertyActionsMenu: 'Open Property Actions',

View File

@@ -62,7 +62,7 @@ export class UmbCopyToClipboardPropertyAction extends UmbPropertyActionBase<Meta
const propertyEditorUiIcon = this.#propertyContext.getEditorManifest()?.meta.icon;
this.#clipboardContext.write({
await this.#clipboardContext.write({
name: entryName,
icon: propertyEditorUiIcon,
propertyValue,

View File

@@ -17,6 +17,8 @@ import { UMB_PROPERTY_CONTEXT, UmbPropertyValueCloneController } from '@umbraco-
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor';
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
/**
* Clipboard context for managing clipboard entries for property values
@@ -26,6 +28,7 @@ import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
*/
export class UmbClipboardPropertyContext extends UmbContextBase {
#init?: Promise<unknown>;
#localize = new UmbLocalizationController(this);
constructor(host: UmbControllerHost) {
super(host, UMB_CLIPBOARD_PROPERTY_CONTEXT);
@@ -104,7 +107,25 @@ export class UmbClipboardPropertyContext extends UmbContextBase {
icon: args.icon,
};
return await clipboardContext.write(entryPreset);
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
if (!notificationContext) {
throw new Error('Notification context is required');
}
try {
const clipboardEntry = await clipboardContext.write(entryPreset);
notificationContext.peek('positive', {
data: { message: this.#localize.term('clipboard_copySuccessHeadline') },
});
return clipboardEntry;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
notificationContext.peek('danger', { data: { message: errorMessage } });
}
return undefined;
}
/**

View File

@@ -1,9 +1,7 @@
import { UmbEntityContext } from '../../entity/entity.context.js';
import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
import { html, nothing, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UmbSectionSidebarContext } from '@umbraco-cms/backoffice/section';
import { UMB_SECTION_SIDEBAR_CONTEXT } from '@umbraco-cms/backoffice/section';
import { html, nothing, customElement, property, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
@@ -34,19 +32,9 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
@state()
_dropdownIsOpen = false;
#sectionSidebarContext?: UmbSectionSidebarContext;
// TODO: provide the entity context on a higher level, like the root element of this entity, tree-item/workspace/... [NL]
#entityContext = new UmbEntityContext(this);
constructor() {
super();
this.consumeContext(UMB_SECTION_SIDEBAR_CONTEXT, (sectionContext) => {
this.#sectionSidebarContext = sectionContext;
});
}
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
if (_changedProperties.has('entityType') && _changedProperties.has('unique')) {
this.#entityContext.setEntityType(this.entityType);
@@ -80,24 +68,7 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
this._firstActionHref = await this._firstActionApi?.getHref();
}
#openContextMenu() {
if (!this.entityType) throw new Error('Entity type is not defined');
if (this.unique === undefined) throw new Error('Unique is not defined');
if (this.#sectionSidebarContext) {
this.#sectionSidebarContext.toggleContextMenu(this, {
entityType: this.entityType,
unique: this.unique,
headline: this.label,
});
} else {
this._dropdownIsOpen = !this._dropdownIsOpen;
}
}
async #onFirstActionClick(event: PointerEvent) {
this.#sectionSidebarContext?.closeContextMenu();
// skip if href is defined
if (this._firstActionHref) {
return;
@@ -123,19 +94,15 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
#renderMore() {
if (this._numberOfActions === 1) return nothing;
if (this.#sectionSidebarContext) {
return html`<uui-button @click=${this.#openContextMenu} label="Open actions menu">
<uui-symbol-more></uui-symbol-more>
</uui-button>`;
}
return html`
<umb-dropdown .open=${this._dropdownIsOpen} @click=${this.#onDropdownClick} compact hide-expand>
<uui-symbol-more slot="label"></uui-symbol-more>
<umb-entity-action-list
@action-executed=${this.#onActionExecuted}
.entityType=${this.entityType}
.unique=${this.unique}></umb-entity-action-list>
<umb-dropdown id="action-modal" .open=${this._dropdownIsOpen} @click=${this.#onDropdownClick} compact hide-expand>
<uui-symbol-more slot="label" label="Open actions menu"></uui-symbol-more>
<uui-scroll-container>
<umb-entity-action-list
@action-executed=${this.#onActionExecuted}
.entityType=${this.entityType}
.unique=${this.unique}></umb-entity-action-list>
</uui-scroll-container>
</umb-dropdown>
`;
}
@@ -149,6 +116,14 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
<uui-icon name=${ifDefined(this._firstActionManifest?.meta.icon)}></uui-icon>
</uui-button>`;
}
static override styles = [
css`
uui-scroll-container {
max-height: 700px;
}
`,
];
}
declare global {

View File

@@ -90,6 +90,7 @@ export class UmbEntityActionListElement extends UmbLitElement {
static override styles = [
css`
:host {
--uui-menu-item-indent: 0;
--uui-menu-item-flat-structure: 1;
}
`,

View File

@@ -10,9 +10,9 @@ export interface UmbPickerModalData<ItemType> {
search?: UmbPickerModalSearchConfig;
}
export interface UmbPickerModalSearchConfig {
export interface UmbPickerModalSearchConfig<QueryParamsType = Record<string, unknown>> {
providerAlias: string;
queryParams?: object;
queryParams?: QueryParamsType;
}
export interface UmbPickerModalValue {

View File

@@ -183,8 +183,10 @@ export class UmbPickerSearchManager<
}
const args = {
searchFrom: this.#config?.searchFrom,
...query,
// ensure that config params are always included
...this.#config?.queryParams,
searchFrom: this.#config?.searchFrom,
};
const { data } = await this.#searchProvider.search(args);

View File

@@ -1,6 +1,7 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbPickerSearchManagerConfig {
export interface UmbPickerSearchManagerConfig<QueryParamsType = Record<string, unknown>> {
providerAlias: string;
searchFrom?: UmbEntityModel;
queryParams?: QueryParamsType;
}

View File

@@ -58,13 +58,6 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
...this.data.search,
searchFrom: this.data.startNode,
});
const searchQueryParams = this.data.search.queryParams;
if (searchQueryParams) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore - TODO wire up types
this.#pickerContext.search.setQuery(searchQueryParams);
}
}
const multiple = this.data?.multiple ?? false;

View File

@@ -0,0 +1,103 @@
using Microsoft.AspNetCore.Http;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.DeliveryApi.Request;
[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
public class ApiContentResponseBuilderTests : UmbracoIntegrationTest
{
private IContentService ContentService => GetRequiredService<IContentService>();
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
private IApiContentResponseBuilder ApiContentResponseBuilder => GetRequiredService<IApiContentResponseBuilder>();
protected IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService<IUmbracoContextAccessor>();
protected IUmbracoContextFactory UmbracoContextFactory => GetRequiredService<IUmbracoContextFactory>();
protected IVariationContextAccessor VariationContextAccessor => GetRequiredService<IVariationContextAccessor>();
protected override void CustomTestSetup(IUmbracoBuilder builder)
{
builder.AddUmbracoHybridCache();
builder.AddDeliveryApi();
}
[SetUp]
public void SetUpTest()
{
var httpContextAccessor = GetRequiredService<IHttpContextAccessor>();
httpContextAccessor.HttpContext = new DefaultHttpContext
{
Request =
{
Scheme = "https",
Host = new HostString("localhost"),
Path = "/",
QueryString = new QueryString(string.Empty)
},
RequestServices = Services
};
}
[Test]
public async Task ContentBuilder_MapsContentDatesCorrectlyForCultureVariance()
{
await GetRequiredService<ILanguageService>().CreateAsync(new Language("da-DK", "Danish"), Constants.Security.SuperUserKey);
var contentType = new ContentTypeBuilder()
.WithAlias("theContentType")
.WithContentVariation(ContentVariation.Culture)
.Build();
contentType.AllowedAsRoot = true;
await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey);
var content = new ContentBuilder()
.WithContentType(contentType)
.WithCultureName("en-US", "Content EN")
.WithCultureName("da-DK", "Content DA")
.Build();
ContentService.Save(content);
ContentService.Publish(content, ["*"]);
Thread.Sleep(200);
content.SetCultureName("Content DA updated", "da-DK");
ContentService.Save(content);
ContentService.Publish(content, ["da-DK"]);
RefreshContentCache();
UmbracoContextAccessor.Clear();
var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext;
var publishedContent = umbracoContext.Content.GetById(content.Key);
Assert.IsNotNull(publishedContent);
VariationContextAccessor.VariationContext = new VariationContext(culture: "en-US");
var enResult = ApiContentResponseBuilder.Build(publishedContent);
Assert.IsNotNull(enResult);
VariationContextAccessor.VariationContext = new VariationContext(culture: "da-DK");
var daResult = ApiContentResponseBuilder.Build(publishedContent);
Assert.IsNotNull(daResult);
Assert.GreaterOrEqual((daResult.UpdateDate - enResult.UpdateDate).TotalMilliseconds, 200);
}
private void RefreshContentCache()
=> GetRequiredService<ContentCacheRefresher>().Refresh([new ContentCacheRefresher.JsonPayload { ChangeTypes = TreeChangeTypes.RefreshAll }]);
}

View File

@@ -1,11 +1,13 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Tests.Common;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
@@ -42,7 +44,7 @@ public class ContentBuilderTests : DeliveryApiTests
var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings(), navigationQueryService: navigationQueryServiceMock.Object);
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor());
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
var result = builder.Build(content.Object);
Assert.NotNull(result);
@@ -57,6 +59,45 @@ public class ContentBuilderTests : DeliveryApiTests
Assert.AreEqual(new DateTime(2023, 07, 12), result.UpdateDate);
}
[TestCase("en-US", "2023-08-04")]
[TestCase("da-DK", "2023-09-08")]
public void ContentBuilder_MapsContentDatesCorrectlyForCultureVariance(string culture, string expectedUpdateDate)
{
var content = new Mock<IPublishedContent>();
var contentType = new Mock<IPublishedContentType>();
contentType.SetupGet(c => c.Alias).Returns("thePageType");
contentType.SetupGet(c => c.ItemType).Returns(PublishedItemType.Content);
contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture);
var key = Guid.NewGuid();
var urlSegment = "url-segment";
var name = "The page";
ConfigurePublishedContentMock(content, key, name, urlSegment, contentType.Object, []);
content.SetupGet(c => c.CreateDate).Returns(new DateTime(2023, 07, 02));
content
.SetupGet(c => c.Cultures)
.Returns(new Dictionary<string, PublishedCultureInfo>
{
{ "en-US", new PublishedCultureInfo("en-US", "EN Name", "en-url-segment", new DateTime(2023, 08, 04)) },
{ "da-DK", new PublishedCultureInfo("da-DK", "DA Name", "da-url-segment", new DateTime(2023, 09, 08)) },
});
var routeBuilder = new Mock<IApiContentRouteBuilder>();
routeBuilder
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));
var variationContextAccessor = new TestVariationContextAccessor { VariationContext = new VariationContext(culture) };
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), variationContextAccessor);
var result = builder.Build(content.Object);
Assert.NotNull(result);
Assert.AreEqual(new DateTime(2023, 07, 02), result.CreateDate);
Assert.AreEqual(DateTime.Parse(expectedUpdateDate), result.UpdateDate);
}
[Test]
public void ContentBuilder_CanCustomizeContentNameInDeliveryApiOutput()
{
@@ -75,7 +116,7 @@ public class ContentBuilderTests : DeliveryApiTests
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns(new ApiContentRoute(content.Object.UrlSegment!, new ApiContentStartItem(Guid.NewGuid(), "/")));
var builder = new ApiContentBuilder(customNameProvider.Object, routeBuilder.Object, CreateOutputExpansionStrategyAccessor());
var builder = new ApiContentBuilder(customNameProvider.Object, routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
var result = builder.Build(content.Object);
Assert.NotNull(result);
@@ -97,7 +138,7 @@ public class ContentBuilderTests : DeliveryApiTests
.Setup(r => r.Build(content.Object, It.IsAny<string?>()))
.Returns((ApiContentRoute)null);
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor());
var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder.Object, CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor());
var result = builder.Build(content.Object);
Assert.Null(result);

View File

@@ -19,7 +19,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests
new ApiContentBuilder(
nameProvider ?? new ApiContentNameProvider(),
CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()),
CreateOutputExpansionStrategyAccessor()));
CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor()));
[Test]
public void ContentPickerValueConverter_BuildsDeliveryApiOutput()

View File

@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Navigation;
using Umbraco.Cms.Tests.Common;
using Umbraco.Extensions;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
@@ -92,6 +93,8 @@ public class DeliveryApiTests
protected IOutputExpansionStrategyAccessor CreateOutputExpansionStrategyAccessor() => new NoopOutputExpansionStrategyAccessor();
protected IVariationContextAccessor CreateVariationContextAccessor() => new TestVariationContextAccessor();
protected IOptions<GlobalSettings> CreateGlobalSettings(bool hideTopLevelNodeFromPath = true)
{
var globalSettings = new GlobalSettings { HideTopLevelNodeFromPath = hideTopLevelNodeFromPath };

View File

@@ -25,7 +25,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest
return new MultiNodeTreePickerValueConverter(
Mock.Of<IUmbracoContextAccessor>(),
Mock.Of<IMemberService>(),
new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor),
new ApiContentBuilder(contentNameProvider, routeBuilder, expansionStrategyAccessor, CreateVariationContextAccessor()),
new ApiMediaBuilder(contentNameProvider, apiUrProvider, Mock.Of<IPublishedValueFallback>(), expansionStrategyAccessor),
CacheManager.Content,
CacheManager.Media,

View File

@@ -43,7 +43,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
public void OutputExpansionStrategy_ExpandsNothingByDefault()
{
var accessor = CreateOutputExpansionStrategyAccessor(false);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();
var prop1 = new PublishedElementPropertyBase(DeliveryApiPropertyType, content.Object, false, PropertyCacheLevel.None, VariationContext, CacheManager);
@@ -69,7 +69,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
public void OutputExpansionStrategy_CanExpandSpecificContent()
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "contentPickerTwo" });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();
@@ -142,7 +142,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
public void OutputExpansionStrategy_CanExpandAllContent()
{
var accessor = CreateOutputExpansionStrategyAccessor(true);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();
@@ -177,7 +177,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
public void OutputExpansionStrategy_DoesNotExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { rootPropertyTypeAlias, nestedPropertyTypeAlias });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();
@@ -207,7 +207,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
public void OutputExpansionStrategy_DoesNotExpandElementsByDefault()
{
var accessor = CreateOutputExpansionStrategyAccessor(false);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var contentPickerValue = CreateSimplePickedContent(111, 222);
@@ -283,7 +283,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe
public void OutputExpansionStrategy_ForwardsExpansionStateToPropertyValueConverter(bool expanding)
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { expanding ? "theAlias" : "noSuchAlias" });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();

View File

@@ -20,7 +20,7 @@ public class OutputExpansionStrategyTests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanExpandSpecifiedElement()
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "element" });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var contentPickerValue = CreateSimplePickedContent(111, 222);
@@ -63,7 +63,7 @@ public class OutputExpansionStrategyTests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanExpandAllElements()
{
var accessor = CreateOutputExpansionStrategyAccessor(true );
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var contentPickerValue = CreateSimplePickedContent(111, 222);
@@ -120,7 +120,7 @@ public class OutputExpansionStrategyTests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_DoesNotExpandElementNestedContentPicker()
{
var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "element" });
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var nestedContentPickerValue = CreateSimplePickedContent(111, 222);

View File

@@ -22,7 +22,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanExpandNestedContentPicker(string rootPropertyTypeAlias, string nestedPropertyTypeAlias)
{
var accessor = CreateOutputExpansionStrategyAccessor($"properties[{rootPropertyTypeAlias}[properties[{nestedPropertyTypeAlias}]]]");
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();
@@ -55,7 +55,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
{
// var accessor = CreateOutputExpansionStrategyAccessor(false, new[] { "element" });
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[$all]]]");
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var contentPickerValue = CreateSimplePickedContent(111, 222);
@@ -98,7 +98,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanExpandAllElements()
{
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[$all]],element2[properties[$all]]]" );
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var contentPickerValue = CreateSimplePickedContent(111, 222);
@@ -155,7 +155,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_DoesNotExpandElementNestedContentPicker()
{
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[contentPicker]]]" );
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var nestedContentPickerValue = CreateSimplePickedContent(111, 222);
@@ -187,7 +187,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanExpandElementNestedContentPicker()
{
var accessor = CreateOutputExpansionStrategyAccessor("properties[element[properties[contentPicker[properties[nestedContentPicker]]]]]");
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var apiElementBuilder = new ApiElementBuilder(accessor);
var nestedContentPickerValue = CreateSimplePickedContent(111, 222);
@@ -221,7 +221,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanExpandContentPickerBeyondTwoLevels()
{
var accessor = CreateOutputExpansionStrategyAccessor($"properties[level1Picker[properties[level2Picker[properties[level3Picker[properties[level4Picker]]]]]]]");
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();
@@ -268,7 +268,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanLimitDirectFields(string includedField)
{
var accessor = CreateOutputExpansionStrategyAccessor(fields: $"properties[{includedField}]");
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = CreateSimplePickedContent(123, 456);
@@ -284,7 +284,7 @@ public class OutputExpansionStrategyV2Tests : OutputExpansionStrategyTestBase
public void OutputExpansionStrategy_CanLimitFieldsOfExpandedContent(bool expand)
{
var accessor = CreateOutputExpansionStrategyAccessor(expand ? "properties[$all]" : null, "properties[contentPickerOne[properties[numberOne]],contentPickerTwo[properties[numberTwo]]]");
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor);
var apiContentBuilder = new ApiContentBuilder(new ApiContentNameProvider(), ApiContentRouteBuilder(), accessor, CreateVariationContextAccessor());
var content = new Mock<IPublishedContent>();