Merge remote-tracking branch 'origin/release/16.0' into v16/dev
This commit is contained in:
@@ -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+";
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }]);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -19,7 +19,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests
|
||||
new ApiContentBuilder(
|
||||
nameProvider ?? new ApiContentNameProvider(),
|
||||
CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()),
|
||||
CreateOutputExpansionStrategyAccessor()));
|
||||
CreateOutputExpansionStrategyAccessor(), CreateVariationContextAccessor()));
|
||||
|
||||
[Test]
|
||||
public void ContentPickerValueConverter_BuildsDeliveryApiOutput()
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user