From e959850a6604a04c7b0fda113ead76eceab38d14 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 16 May 2025 12:30:29 +0200 Subject: [PATCH] Introduce Variant Context (#19334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add variant context * set culture in variant context when changing app language * set variant context when swithing variant in a workspace * Update content-detail-workspace-base.ts * clean up * remove from split view manager * Update property-dataset-base-context.ts * change name to fallbackCulture * simplify * make all methods async * Update current-user-action.extension.ts * remove culture and segment state --------- Co-authored-by: Niels Lyngsø --- .../element-property-dataset.context.ts | 5 +- .../property-dataset-base-context.ts | 12 +- .../src/packages/core/variant/constants.ts | 1 + .../core/variant/context/constants.ts | 1 + .../packages/core/variant/context/index.ts | 1 + .../variant/context/variant.context.token.ts | 4 + .../core/variant/context/variant.context.ts | 110 ++++++++++++++++ .../src/packages/core/variant/index.ts | 2 + ...iant-workspace-property-dataset-context.ts | 4 +- .../item/document-item-data-resolver.ts | 124 ++++++++---------- .../global-contexts/app-language.context.ts | 31 +++-- 11 files changed, 209 insertions(+), 86 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts index 96381eb39f..70a25951bd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/property-dataset-context/element-property-dataset.context.ts @@ -12,7 +12,7 @@ import { createObservablePart, mergeObservables, } from '@umbraco-cms/backoffice/observable-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; @@ -47,6 +47,8 @@ export abstract class UmbElementPropertyDatasetContext< protected _readOnly = new UmbBooleanState(false); public readOnly = this._readOnly.asObservable(); + #variantContext = new UmbVariantContext(this); + getEntityType(): string { return this._dataOwner.getEntityType(); } @@ -64,6 +66,7 @@ export abstract class UmbElementPropertyDatasetContext< super(host, UMB_PROPERTY_DATASET_CONTEXT); this._dataOwner = dataOwner; this.#variantId = variantId ?? UmbVariantId.CreateInvariant(); + this.#variantContext.setVariantId(this.#variantId); this.#propertyVariantIdPromise = new Promise((resolve) => { this.#propertyVariantIdPromiseResolver = resolve as any; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts index 9fbdd695bf..72a45a7648 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts @@ -5,7 +5,7 @@ import type { UmbNameablePropertyDatasetContext } from './nameable-property-data import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbArrayState, UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant'; /** * A base property dataset context implementation. @@ -32,26 +32,34 @@ export class UmbPropertyDatasetContextBase #readOnly = new UmbBooleanState(false); public readOnly = this.#readOnly.asObservable(); + #variantId: UmbVariantId = UmbVariantId.CreateInvariant(); + #variantContext = new UmbVariantContext(this); + getEntityType() { return this._entityType; } + getUnique() { return this._unique; } + getName() { return this.#name.getValue(); } + setName(name: string | undefined) { this.#name.setValue(name); } + getVariantId() { - return UmbVariantId.CreateInvariant(); + return this.#variantId; } // variant id for a specific property? constructor(host: UmbControllerHost) { // The controller alias, is a very generic name cause we want only one of these for this controller host. super(host, UMB_PROPERTY_DATASET_CONTEXT); + this.#variantContext.setVariantId(this.getVariantId()); } /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts new file mode 100644 index 0000000000..9c6a2415c8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/constants.ts @@ -0,0 +1 @@ +export * from './context/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts new file mode 100644 index 0000000000..0b49906ceb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/constants.ts @@ -0,0 +1 @@ +export { UMB_VARIANT_CONTEXT } from './variant.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts new file mode 100644 index 0000000000..85a9b48dce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/index.ts @@ -0,0 +1 @@ +export { UmbVariantContext } from './variant.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts new file mode 100644 index 0000000000..d2733dac1b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.token.ts @@ -0,0 +1,4 @@ +import type { UmbVariantContext } from './variant.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_VARIANT_CONTEXT = new UmbContextToken('UmbVariantContext'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts new file mode 100644 index 0000000000..04f9323d96 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/context/variant.context.ts @@ -0,0 +1,110 @@ +import { UmbVariantId } from '../variant-id.class.js'; +import { UMB_VARIANT_CONTEXT } from './variant.context.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbClassState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; + +/** + * A context for the current variant state. + * @class UmbVariantContext + * @augments {UmbContextBase} + * @implements {UmbVariantContext} + */ +export class UmbVariantContext extends UmbContextBase { + #variantId = new UmbClassState(undefined); + public readonly variantId = this.#variantId.asObservable(); + public readonly culture = this.#variantId.asObservablePart((x) => x?.culture); + public readonly segment = this.#variantId.asObservablePart((x) => x?.segment); + + #fallbackCulture = new UmbStringState(undefined); + public fallbackCulture = this.#fallbackCulture.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_VARIANT_CONTEXT); + + this.consumeContext(UMB_VARIANT_CONTEXT, (context) => { + this.observe( + context?.fallbackCulture, + (fallbackCulture) => { + if (!fallbackCulture) return; + this.setFallbackCulture(fallbackCulture); + }, + 'observeFallbackCulture', + ); + }).skipHost(); + } + + /** + * Sets the variant id state + * @param {UmbVariantId | undefined} variantId - The variant to set + * @memberof UmbVariantContext + */ + async setVariantId(variantId: UmbVariantId | undefined): Promise { + this.#variantId.setValue(variantId); + } + + /** + * Gets variant state + * @returns {Promise} - The variant state + * @memberof UmbVariantContext + */ + async getVariantId(): Promise { + return this.#variantId.getValue(); + } + + /** + * Gets the culture state + * @returns {(Promise)} - The culture state + * @memberof UmbVariantContext + */ + async getCulture(): Promise { + return this.#variantId.getValue()?.culture; + } + + /** + * Sets the variant culture state + * @param {string | undefined} culture - The culture to set + * @memberof UmbVariantContext + */ + async setCulture(culture: string | null): Promise { + const variantId = new UmbVariantId(culture, this.#variantId.getValue()?.segment); + this.#variantId.setValue(variantId); + } + + /** + * Gets the variant segment state + * @returns {(Promise)} - The segment state + * @memberof UmbVariantContext + */ + async getSegment(): Promise { + return this.#variantId.getValue()?.segment; + } + + /** + * Sets the variant segment state + * @param {string | undefined} segment - The segment to set + * @memberof UmbVariantContext + */ + async setSegment(segment: string | null): Promise { + const variantId = new UmbVariantId(this.#variantId.getValue()?.culture, segment); + this.#variantId.setValue(variantId); + } + + /** + * Gets the fallback culture state + * @returns {(Promise)} - The fallback culture state + * @memberof UmbVariantContext + */ + async getFallbackCulture(): Promise { + return this.#fallbackCulture.getValue(); + } + + /** + * Sets the fallback culture state + * @param {string | undefined} culture - The fallback culture to set + * @memberof UmbVariantContext + */ + async setFallbackCulture(culture: string | null): Promise { + this.#fallbackCulture.setValue(culture); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts index d190db617d..9d50648397 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts @@ -1,3 +1,5 @@ +export * from './constants.js'; +export * from './context/index.js'; export * from './variant-id.class.js'; export * from './variant-object-compare.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts index 66fd7c0956..d9e055c4e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts @@ -7,7 +7,7 @@ import type { import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbBooleanState, type Observable } from '@umbraco-cms/backoffice/observable-api'; /** @@ -23,6 +23,7 @@ export class UmbInvariantWorkspacePropertyDatasetContext< public readOnly = this.#readOnly.asObservable(); #workspace: WorkspaceType; + #variantContext = new UmbVariantContext(this); name; @@ -49,6 +50,7 @@ export class UmbInvariantWorkspacePropertyDatasetContext< this.#workspace = workspace; this.name = this.#workspace.name; + this.#variantContext.setVariantId(this.getVariantId()); } get properties(): Observable | undefined> { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts index da25606180..d24c17e6f6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/item/document-item-data-resolver.ts @@ -3,10 +3,8 @@ import type { UmbDocumentItemModel } from './types.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; -import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language'; -import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbBooleanState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { type UmbVariantContext, UMB_VARIANT_CONTEXT, type UmbVariantId } from '@umbraco-cms/backoffice/variant'; type UmbDocumentItemDataResolverModel = Omit; @@ -17,51 +15,54 @@ type UmbDocumentItemDataResolverModel = Omit extends UmbControllerBase { - #defaultCulture?: string; - #appCulture?: string; - #propertyDataSetCulture?: UmbVariantId; - #data?: DataType | undefined; + #data = new UmbObjectState(undefined); - #init: Promise; - - #unique = new UmbStringState(undefined); - public readonly unique = this.#unique.asObservable(); + public readonly unique = this.#data.asObservablePart((x) => x?.unique); + public readonly icon = this.#data.asObservablePart((x) => x?.documentType.icon); + public readonly isTrashed = this.#data.asObservablePart((x) => x?.isTrashed); #name = new UmbStringState(undefined); public readonly name = this.#name.asObservable(); - #icon = new UmbStringState(undefined); - public readonly icon = this.#icon.asObservable(); - #state = new UmbStringState(undefined); public readonly state = this.#state.asObservable(); - #isTrashed = new UmbBooleanState(undefined); - public readonly isTrashed = this.#isTrashed.asObservable(); - #isDraft = new UmbBooleanState(undefined); public readonly isDraft = this.#isDraft.asObservable(); + #variantContext?: UmbVariantContext; + #variantId?: UmbVariantId; + #fallbackCulture?: string | null; + constructor(host: UmbControllerHost) { super(host); - this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => { - this.#propertyDataSetCulture = context?.getVariantId(); - this.#setVariantAwareValues(); + this.consumeContext(UMB_VARIANT_CONTEXT, (context) => { + this.#variantContext = context; + this.#observeVariantContext(); }); + } - // We do not depend on this context because we know is it only available in some cases - this.#init = this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (context) => { - this.observe(context?.appLanguageCulture, (culture) => { - this.#appCulture = culture; + #observeVariantContext() { + this.observe( + this.#variantContext?.variantId, + (variantId) => { + if (variantId === undefined) return; + this.#variantId = variantId; this.#setVariantAwareValues(); - }); + }, + 'umbObserveVariantId', + ); - this.observe(context?.appDefaultLanguage, (value) => { - this.#defaultCulture = value?.unique; + this.observe( + this.#variantContext?.fallbackCulture, + (fallbackCulture) => { + if (fallbackCulture === undefined) return; + this.#fallbackCulture = fallbackCulture; this.#setVariantAwareValues(); - }); - }).asPromise(); + }, + 'umbObserveFallbackCulture', + ); } /** @@ -70,7 +71,7 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#unique.getValue(); + return this.#data?.getValue()?.unique; } /** @@ -112,7 +99,6 @@ export class UmbDocumentItemDataResolver { - await this.#init; return this.#name.getValue() || ''; } @@ -122,8 +108,7 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#data?.documentType.icon; + return this.#data?.getValue()?.documentType.icon; } /** @@ -132,8 +117,8 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#getCurrentVariant()?.state; + const variant = await this.#getCurrentVariant(); + return variant?.state; } /** @@ -142,7 +127,6 @@ export class UmbDocumentItemDataResolver { - await this.#init; return this.#isDraft.getValue() ?? false; } @@ -152,52 +136,56 @@ export class UmbDocumentItemDataResolver { - await this.#init; - return this.#data?.isTrashed ?? false; + return this.#data?.getValue()?.isTrashed ?? false; } #setVariantAwareValues() { + if (!this.#variantContext) return; + if (!this.#variantId) return; + if (!this.#fallbackCulture) return; + if (!this.#data) return; this.#setName(); this.#setIsDraft(); this.#setState(); } - #setName() { - const variant = this.#getCurrentVariant(); + async #setName() { + const variant = await this.#getCurrentVariant(); if (variant) { this.#name.setValue(variant.name); return; } - const fallbackName = this.#findVariant(this.#defaultCulture)?.name; + + const fallbackName = this.#findVariant(this.#fallbackCulture!)?.name; this.#name.setValue(`(${fallbackName})`); } - #setIsDraft() { - const variant = this.#getCurrentVariant(); + async #setIsDraft() { + const variant = await this.#getCurrentVariant(); const isDraft = variant?.state === UmbDocumentVariantState.DRAFT || false; this.#isDraft.setValue(isDraft); } - #setState() { - const variant = this.#getCurrentVariant(); + async #setState() { + const variant = await this.#getCurrentVariant(); const state = variant?.state || UmbDocumentVariantState.NOT_CREATED; this.#state.setValue(state); } #findVariant(culture: string | undefined) { - return this.#data?.variants.find((x) => x.culture === culture); + return this.getData()?.variants.find((x) => x.culture === culture); } - #getCurrentVariant() { + async #getCurrentVariant() { if (this.#isInvariant()) { - return this.#data?.variants?.[0]; + return this.getData()?.variants?.[0]; } - const culture = this.#propertyDataSetCulture?.culture || this.#appCulture; - return this.#findVariant(culture); + const culture = this.#variantId?.culture; + return this.#findVariant(culture!); } #isInvariant() { - return this.#data?.variants?.[0]?.culture === null; + return this.getData()?.variants?.[0]?.culture === null; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts index 684686c1cf..636c03eaa0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts @@ -8,6 +8,7 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import { UmbReadOnlyStateManager } from '@umbraco-cms/backoffice/utils'; import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth'; +import { UmbVariantContext } from '@umbraco-cms/backoffice/variant'; // TODO: Make a store for the App Languages. // TODO: Implement default language end-point, in progress at backend team, so we can avoid getting all languages. @@ -51,6 +52,8 @@ export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { #readOnlyStateIdentifier = 'UMB_LANGUAGE_PERMISSION_'; #localStorageKey = 'umb:appLanguage'; + #variantContext = new UmbVariantContext(this); + constructor(host: UmbControllerHost) { super(host, UMB_APP_LANGUAGE_CONTEXT); @@ -96,6 +99,9 @@ export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { // set the new language this.#appLanguage.update(language); + // Update the variant context with the new language + this.#variantContext.setCulture(language.unique); + // store the new language in local storage localStorage.setItem(this.#localStorageKey, language?.unique); @@ -119,23 +125,20 @@ export class UmbAppLanguageContext extends UmbContextBase implements UmbApi { } #initAppLanguage() { - // get the selected language from local storage - const uniqueFromLocalStorage = localStorage.getItem(this.#localStorageKey); - - if (uniqueFromLocalStorage) { - const language = this.#findLanguage(uniqueFromLocalStorage); - if (language) { - this.setLanguage(language.unique); - return; - } - } - - const defaultLanguage = this.#languages.getValue().find((x) => x.isDefault); + const defaultLanguageUnique = this.#languages.getValue().find((x) => x.isDefault)?.unique; // TODO: do we always have a default language? // do we always get the default language on the first request, or could it be on page 2? // in that case do we then need an endpoint to get the default language? - if (!defaultLanguage?.unique) return; - this.setLanguage(defaultLanguage.unique); + if (!defaultLanguageUnique) return; + + this.#variantContext.setFallbackCulture(defaultLanguageUnique); + + // get the selected language from local storage + const uniqueFromLocalStorage = localStorage.getItem(this.#localStorageKey); + const languageFromLocalStorage = this.#findLanguage(uniqueFromLocalStorage || ''); + const languageUniqueToSet = languageFromLocalStorage ? languageFromLocalStorage.unique : defaultLanguageUnique; + + this.setLanguage(languageUniqueToSet); } #findLanguage(unique: string) {