diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts index 9451be884d..4f1f5a9d49 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts @@ -8,11 +8,11 @@ import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observab * @export * @class UmbSelectionManager */ -export class UmbSelectionManager extends UmbBaseController { +export class UmbSelectionManager extends UmbBaseController { #selectable = new UmbBooleanState(false); public readonly selectable = this.#selectable.asObservable(); - #selection = new UmbArrayState(>[], (x) => x); + #selection = new UmbArrayState(>[], (x) => x); public readonly selection = this.#selection.asObservable(); #multiple = new UmbBooleanState(false); @@ -51,10 +51,10 @@ export class UmbSelectionManager extends UmbBaseController { /** * Sets the current selection. - * @param {Array} value + * @param {Array} value * @memberof UmbSelectionManager */ - public setSelection(value: Array) { + public setSelection(value: Array) { if (this.getSelectable() === false) return; if (value === undefined) throw new Error('Value cannot be undefined'); const newSelection = this.getMultiple() ? value : value.slice(0, 1); @@ -87,20 +87,20 @@ export class UmbSelectionManager extends UmbBaseController { /** * Toggles the given unique id in the current selection. - * @param {(string | null)} unique + * @param {(ValueType)} unique * @memberof UmbSelectionManager */ - public toggleSelect(unique: string | null) { + public toggleSelect(unique: ValueType) { if (this.getSelectable() === false) return; this.isSelected(unique) ? this.deselect(unique) : this.select(unique); } /** * Appends the given unique id to the current selection. - * @param {(string | null)} unique + * @param {(ValueType)} unique * @memberof UmbSelectionManager */ - public select(unique: string | null) { + public select(unique: ValueType) { if (this.getSelectable() === false) return; if (this.isSelected(unique)) return; const newSelection = this.getMultiple() ? [...this.getSelection(), unique] : [unique]; @@ -110,10 +110,10 @@ export class UmbSelectionManager extends UmbBaseController { /** * Removes the given unique id from the current selection. - * @param {(string | null)} unique + * @param {(ValueType)} unique * @memberof UmbSelectionManager */ - public deselect(unique: string | null) { + public deselect(unique: ValueType) { if (this.getSelectable() === false) return; const newSelection = this.getSelection().filter((x) => x !== unique); this.#selection.setValue(newSelection); @@ -122,11 +122,11 @@ export class UmbSelectionManager extends UmbBaseController { /** * Returns true if the given unique id is selected. - * @param {(string | null)} unique + * @param {(ValueType)} unique * @return {*} * @memberof UmbSelectionManager */ - public isSelected(unique: string | null) { + public isSelected(unique: ValueType) { return this.getSelection().includes(unique); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts index 89b4a59249..ee32d57e86 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts @@ -11,4 +11,8 @@ export interface UmbVariantModel { export interface UmbVariantOptionModel { variant?: VariantType; language: UmbLanguageDetailModel; + /** + * The unique identifier is a VariantId string. + */ + unique: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/variant-id.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/variant-id.class.ts index b2f9af347f..2e6144b1c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/variant-id.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/variant-id.class.ts @@ -10,6 +10,10 @@ export function variantPropertiesObjectToString(variant: UmbObjectWithVariantPro export const UMB_INVARIANT_CULTURE = 'invariant'; +/** + * An identifier representing a Variant. This is at current state a culture and a segment. + * The identifier is not specific for ContentType Variants, but is used for many type of identification of a culture and a segment. One case is any property of a ContentType can be resolved into a VariantId depending on their structural settings such as Vary by Culture and Vary by Segmentation. + */ export class UmbVariantId { public static Create(variantData: UmbObjectWithVariantProperties): UmbVariantId { return Object.freeze(new UmbVariantId(variantData.culture, variantData.segment)); @@ -30,7 +34,7 @@ export class UmbVariantId { public readonly segment: string | null = null; public readonly schedule: { publishTime?: string | null; unpublishTime?: string | null } | null = null; - constructor(culture: string | null, segment: string | null) { + constructor(culture?: string | null, segment?: string | null) { this.culture = (culture === UMB_INVARIANT_CULTURE ? null : culture?.toLowerCase()) ?? null; this.segment = segment ?? null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts index d04c0497a2..1bc79d33fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/variant-selector/variant-selector.element.ts @@ -5,15 +5,12 @@ import { UUIInputEvent, type UUIPopoverContainerElement, } from '@umbraco-cms/backoffice/external/uui'; -import { css, html, nothing, customElement, state, ifDefined, query } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, nothing, customElement, state, query } from '@umbraco-cms/backoffice/external/lit'; import { UMB_WORKSPACE_SPLIT_VIEW_CONTEXT, type ActiveVariant } from '@umbraco-cms/backoffice/workspace'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; -import type { UmbDocumentVariantModel } from '@umbraco-cms/backoffice/document'; -import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; -import { UmbArrayState, combineObservables } from '@umbraco-cms/backoffice/observable-api'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document'; type UmbDocumentVariantOption = { culture: string | null; @@ -38,9 +35,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { _activeVariants: Array = []; @state() - get _activeVariantsCultures(): string[] { - return this._activeVariants.map((el) => el.culture ?? '') ?? []; - } + _activeVariantsCultures: string[] = []; #splitViewContext?: typeof UMB_WORKSPACE_SPLIT_VIEW_CONTEXT.TYPE; #datasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE; @@ -57,9 +52,6 @@ export class UmbVariantSelectorElement extends UmbLitElement { @state() private _variantSelectorOpen = false; - #languageRepository = new UmbLanguageCollectionRepository(this); - #languages = new UmbArrayState([], (x) => x.unique); - constructor() { super(); @@ -67,37 +59,36 @@ export class UmbVariantSelectorElement extends UmbLitElement { this.#splitViewContext = instance; this.#observeVariants(); this.#observeActiveVariants(); + this.#observeCurrentVariant(); }); this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (instance) => { this.#datasetContext = instance; this.#observeDatasetContext(); + this.#observeCurrentVariant(); }); - - this.#loadLanguages(); - } - - async #loadLanguages() { - const { data: languages } = await this.#languageRepository.requestCollection({}); - if (!languages) return; - this.#languages.setValue(languages.items); } async #observeVariants() { if (!this.#splitViewContext) return; - const workspaceContext = this.#splitViewContext.getWorkspaceContext(); + // NOTICE: This is dirty (the TypeScript casting), we can only accept doing this so far because we currently only use the Variant Selector on Document Workspace. [NL] + // This would need a refactor to enable the code below to work with different ContentTypes. Main problem here is the state, which is not generic for them all. [NL] + const workspaceContext = this.#splitViewContext.getWorkspaceContext() as UmbDocumentWorkspaceContext; if (!workspaceContext) throw new Error('Split View Workspace context not found'); this.observe( - workspaceContext.allowedVariants, - (variants) => { - this._variants = variants.map((variant) => { + workspaceContext.variantOptions, + (options) => { + this._variants = options.map((option) => { + const name = option.variant?.name ?? option.language.name; + const segment = option.variant?.segment ?? null; return { - culture: variant.culture, - segment: variant.segment, - title: variant.name + (variant.segment ? ` — ${variant.segment}` : ''), - displayName: variant.name + (variant.segment ? ` — ${variant.segment}` : ''), - state: (variant as UmbDocumentVariantModel).state ?? DocumentVariantStateModel.NOT_CREATED, + // Notice the option object has a unique property, but it's not used here. (Its equivalent to a UmbVariantId string) [NL] + culture: option.language.unique, + segment: segment, + title: name + (segment ? ` — ${segment}` : ''), + displayName: name + (segment ? ` — ${segment}` : ''), + state: option.variant?.state ?? DocumentVariantStateModel.NOT_CREATED, }; }); }, @@ -115,6 +106,7 @@ export class UmbVariantSelectorElement extends UmbLitElement { (activeVariants) => { if (activeVariants) { this._activeVariants = activeVariants; + this._activeVariantsCultures = this._activeVariants.map((el) => el.culture ?? '') ?? []; } }, '_observeActiveVariants', @@ -124,24 +116,6 @@ export class UmbVariantSelectorElement extends UmbLitElement { async #observeDatasetContext() { if (!this.#datasetContext) return; - - const variantId = this.#datasetContext.getVariantId(); - - const culture = variantId.culture; - const segment = variantId.segment; - - this.observe( - this.#languages.asObservablePart( - (languages) => languages.find((language) => language.unique === variantId.culture)?.name ?? '', - ), - (languageName) => { - this._variantDisplayName = (languageName ? languageName : '') + (segment ? ' — ' + segment : ''); - this._variantTitleName = - (languageName ? `${languageName} (${culture})` : '') + (segment ? ' — ' + segment : ''); - }, - '_languages', - ); - this.observe( this.#datasetContext.name, (name) => { @@ -151,6 +125,30 @@ export class UmbVariantSelectorElement extends UmbLitElement { ); } + async #observeCurrentVariant() { + if (!this.#datasetContext || !this.#splitViewContext) return; + const workspaceContext = this.#splitViewContext.getWorkspaceContext(); + if (!workspaceContext) return; + + const variantId = this.#datasetContext.getVariantId(); + // Find the variant option matching this, to get the language name... + + const culture = variantId.culture; + const segment = variantId.segment; + + this.observe( + workspaceContext.variantOptions, + (options) => { + const option = options.find((option) => option.language.unique === culture); + const languageName = option?.language.name; + this._variantDisplayName = (languageName ? languageName : '') + (segment ? ` — ${segment}` : ''); + this._variantTitleName = + (languageName ? `${languageName} (${culture})` : '') + (segment ? ` — ${segment}` : ''); + }, + '_currentLanguage', + ); + } + #handleInput(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts index a40153c268..341f6b897a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts @@ -2,17 +2,18 @@ import type { UmbWorkspaceSplitViewManager } from '../workspace-split-view-manag import type { UmbPropertyDatasetContext } from '../../property/property-dataset/property-dataset-context.interface.js'; import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -import type { UmbVariantId, UmbVariantModel } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantId, UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export interface UmbVariantableWorkspaceContextInterface extends UmbSaveableWorkspaceContextInterface { +export interface UmbVariantableWorkspaceContextInterface + extends UmbSaveableWorkspaceContextInterface { // Name: getName(variantId?: UmbVariantId): string | undefined; setName(name: string, variantId?: UmbVariantId): void; // Variant: variants: Observable>; - allowedVariants: Observable>; + variantOptions: Observable>>; splitView: UmbWorkspaceSplitViewManager; getVariant(variantId: UmbVariantId): UmbVariantModel | undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-split-view-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-split-view-manager.class.ts index a48efbfe20..63bbf04d0a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-split-view-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-split-view-manager.class.ts @@ -70,7 +70,7 @@ export class UmbWorkspaceSplitViewManager { const currentVariant = this.getActiveVariants()[0]; const workspaceRoute = this.getWorkspaceRoute(); if (currentVariant && workspaceRoute) { - history.pushState(null, '', `${workspaceRoute}/${new UmbVariantId(currentVariant)}_&_${newVariant.toString()}`); + history.pushState(null, '', `${workspaceRoute}/${UmbVariantId.Create(currentVariant)}_&_${newVariant}`); return true; } return false; @@ -83,7 +83,7 @@ export class UmbWorkspaceSplitViewManager { if (activeVariants && index < activeVariants.length) { const newVariants = activeVariants.filter((x) => x.index !== index); - const variantPart: string = newVariants.map((v) => new UmbVariantId(v).toString()).join('_&_'); + const variantPart: string = newVariants.map((v) => UmbVariantId.Create(v)).join('_&_'); history.pushState(null, '', `${workspaceRoute}/${variantPart}`); return true; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts index d7c794512d..0276e0f366 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/publish.action.ts @@ -1,21 +1,10 @@ -import { UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT } from '../global-contexts/index.js'; import type { UmbDocumentPublishingRepository } from '../repository/index.js'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; -import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; export class UmbPublishDocumentEntityAction extends UmbEntityActionBase { - #variantManagerContext?: typeof UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT.TYPE; - - constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) { - super(host, repositoryAlias, unique, entityType); - - this.consumeContext(UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT, (context) => { - this.#variantManagerContext = context; - }); - } - async execute() { - if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); - await this.#variantManagerContext.publish(this.unique); + throw new Error('This action not implemented.'); + //if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); + //await this.#variantManagerContext.publish(this.unique); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts index 978963da48..cf34e477e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/unpublish.action.ts @@ -1,21 +1,10 @@ -import { UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT } from '../global-contexts/index.js'; import type { UmbDocumentPublishingRepository } from '../repository/index.js'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; -import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase { - #variantManagerContext?: typeof UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT.TYPE; - - constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) { - super(host, repositoryAlias, unique, entityType); - - this.consumeContext(UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT, (context) => { - this.#variantManagerContext = context; - }); - } - async execute() { - if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); - await this.#variantManagerContext.unpublish(this.unique); + throw new Error('This action not implemented.'); + //if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); + //await this.#variantManagerContext.unpublish(this.unique); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-variant-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-variant-manager.context.temp_ts similarity index 93% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-variant-manager.context.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-variant-manager.context.temp_ts index 5a847afd50..d5323f8a55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-variant-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-variant-manager.context.temp_ts @@ -1,4 +1,4 @@ -import { UmbDocumentVariantState, type UmbDocumentVariantModel } from '../types.js'; +import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../types.js'; import { UmbDocumentDetailRepository } from '../repository/detail/document-detail.repository.js'; import { UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, @@ -47,9 +47,9 @@ export class UmbDocumentVariantManagerContext * @returns The selected variants to perform the operation on. */ async pickVariants( - availableVariants: Array, type: UmbDocumentVariantPickerModalData['type'], - activeVariantCultures?: Array, + availableVariants: Array, + activeVariantCultures?: Array, ): Promise { // If there is only one variant, we don't need to select anything. if (availableVariants.length === 1) { @@ -60,7 +60,7 @@ export class UmbDocumentVariantManagerContext const modalData: UmbDocumentVariantPickerModalData = { type, - variants: availableVariants, + options: availableVariants, }; const modalContext = this.#modalManagerContext.open(UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, { @@ -75,6 +75,7 @@ export class UmbDocumentVariantManagerContext const selectedVariants = result.selection.map((x) => x?.toLowerCase() ?? ''); // Match the result to the available variants. + // Why would this be needed if the modal is only showing available variants? [NL] const variantIds = availableVariants .filter((x) => selectedVariants.includes(x.culture!)) .map((x) => UmbVariantId.Create(x)); @@ -90,8 +91,8 @@ export class UmbDocumentVariantManagerContext const { data } = await this.#documentRepository.requestByUnique(documentUnique); if (!data) throw new Error('Document not found'); const variantIds = await this.pickVariants( - data.variants, 'publish', + data.variants, this.#appLanguageCulture ? [this.#appLanguageCulture] : undefined, ); if (variantIds.length) { @@ -111,8 +112,8 @@ export class UmbDocumentVariantManagerContext const variants = data.variants.filter((variant) => variant.state === UmbDocumentVariantState.PUBLISHED); const variantIds = await this.pickVariants( - variants, 'unpublish', + variants, this.#appLanguageCulture ? [this.#appLanguageCulture] : undefined, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/index.ts deleted file mode 100644 index 8eed586555..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './document-variant-manager.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/manifests.ts deleted file mode 100644 index 2b5b972ebb..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/manifests.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ManifestGlobalContext } from '@umbraco-cms/backoffice/extension-registry'; - -export const manifests: Array = [ - { - type: 'globalContext', - alias: 'Umb.GlobalContext.DocumentVariantManager', - name: 'Document Variant Manager Context', - js: () => import('./document-variant-manager.context.js'), - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts index 857816321f..4b900ea615 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts @@ -12,7 +12,6 @@ import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; -import { manifests as globalContextManifests } from './global-contexts/manifests.js'; export const manifests = [ ...breadcrumbManifests, @@ -29,5 +28,4 @@ export const manifests = [ ...treeManifests, ...userPermissionManifests, ...workspaceManifests, - ...globalContextManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.element.ts index 3e85234acd..7d60157b54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.element.ts @@ -1,10 +1,10 @@ -import { type UmbDocumentVariantModel, UmbDocumentVariantState } from '../../types.js'; +import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js'; import type { UmbDocumentVariantPickerModalValue, UmbDocumentVariantPickerModalData, } from './document-variant-picker-modal.token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; @@ -13,7 +13,17 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement< UmbDocumentVariantPickerModalData, UmbDocumentVariantPickerModalValue > { - #selectionManager = new UmbSelectionManager(this); + #selectionManager = new UmbSelectionManager(this); + + @state() + _selection: Array = []; + + constructor() { + super(); + this.observe(this.#selectionManager.selection, (selection) => { + this._selection = selection; + }); + } connectedCallback(): void { super.connectedCallback(); @@ -21,16 +31,18 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement< this.#selectionManager.setMultiple(true); // Make sure all mandatory variants are selected when not in unpublish mode + // TODO: Currently only supports culture variants, not segment variants, but as well our Selection Manager also only supports a single string value pr. selection... [NL] this.#selectionManager.setSelection(this.value?.selection ?? []); + if (this.data?.type !== 'unpublish') { this.#selectMandatoryVariants(); } } #selectMandatoryVariants() { - this.data?.variants.forEach((variant) => { - if (variant.isMandatory) { - this.#selectionManager.select(variant.culture); + this.data?.options.forEach((variant) => { + if (variant.language?.isMandatory) { + this.#selectionManager.select(variant.unique); } }); } @@ -87,17 +99,17 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement< return html`

${this.localize.term(this.#subtitle)}

${repeat( - this.data?.variants ?? [], - (item) => item.culture, - (item) => html` + this.data?.options ?? [], + (option) => option.unique, + (option) => html` this.#selectionManager.select(item.culture)} - @deselected=${() => this.#selectionManager.deselect(item.culture)} - ?selected=${this.#selectionManager.isSelected(item.culture)}> + label=${option.variant?.name ?? option.language.name} + @selected=${() => this.#selectionManager.select(option.unique)} + @deselected=${() => this.#selectionManager.deselect(option.unique)} + ?selected=${this._selection.includes(option.language.unique)}> - ${this.#renderLabel(item)} + ${this.#renderLabel(option)} `, )} @@ -114,11 +126,14 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
`; } - #renderLabel(variant: UmbDocumentVariantModel) { + #renderLabel(option: UmbDocumentVariantOptionModel) { return html`
- ${variant.segment ? variant.segment + ' - ' : ''}${variant.name} -
${this.#renderVariantStatus(variant)}
- ${variant.isMandatory && variant.state !== UmbDocumentVariantState.PUBLISHED + ${option.variant?.segment ? option.variant.segment + ' - ' : ''}${option.variant?.name ?? + option.language.name} +
${this.#renderVariantStatus(option)}
+ ${option.language.isMandatory && option.variant?.state !== UmbDocumentVariantState.PUBLISHED ? html`
Mandatory language
` @@ -126,16 +141,17 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
`; } - #renderVariantStatus(variant: UmbDocumentVariantModel) { - switch (variant.state) { + #renderVariantStatus(option: UmbDocumentVariantOptionModel) { + switch (option.variant?.state) { case UmbDocumentVariantState.PUBLISHED: return this.localize.term('content_published'); case UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES: return this.localize.term('content_publishedPendingChanges'); - case UmbDocumentVariantState.NOT_CREATED: case UmbDocumentVariantState.DRAFT: - default: return this.localize.term('content_unpublished'); + case UmbDocumentVariantState.NOT_CREATED: + default: + return this.localize.term('content_notCreated'); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.stories.ts index 19d38ffac6..0b20dbbaaf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.stories.ts @@ -12,17 +12,29 @@ import { html } from '@umbraco-cms/backoffice/external/lit'; const modalData: UmbDocumentVariantPickerModalData = { type: 'save', - variants: [ + options: [ { - name: 'English', - culture: 'en-us', - state: UmbDocumentVariantState.PUBLISHED, - createDate: '2021-08-25T14:00:00Z', - publishDate: null, - updateDate: null, - segment: null, - isMandatory: true, + unique: 'en-us', + variant: { + name: 'English variant name', + culture: 'en-us', + state: UmbDocumentVariantState.PUBLISHED, + createDate: '2021-08-25T14:00:00Z', + publishDate: null, + updateDate: null, + segment: null, + }, + language: { + entityType: 'language', + name: 'English', + unique: 'en-us', + isDefault: true, + isMandatory: true, + fallbackIsoCode: null, + }, }, + /* + // TODO: We do not support segments currently { name: 'English', culture: 'en-us', @@ -31,17 +43,27 @@ const modalData: UmbDocumentVariantPickerModalData = { publishDate: null, updateDate: null, segment: 'GTM', - isMandatory: true, }, + */ { - name: 'Danish', - culture: 'da-dk', - state: UmbDocumentVariantState.NOT_CREATED, - createDate: null, - publishDate: null, - updateDate: null, - segment: null, - isMandatory: false, + unique: 'da-dk', + variant: { + name: 'Danish variant name', + culture: 'da-dk', + state: UmbDocumentVariantState.NOT_CREATED, + createDate: null, + publishDate: null, + updateDate: null, + segment: null, + }, + language: { + entityType: 'language', + name: 'Danish', + unique: 'da-dk', + isDefault: false, + isMandatory: false, + fallbackIsoCode: null, + }, }, ], }; @@ -80,7 +102,6 @@ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => { publishDate: '2021-08-25T14:00:00Z', updateDate: null, segment: null, - isMandatory: true, }, { name: 'English', @@ -90,7 +111,6 @@ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => { publishDate: '2021-08-25T14:00:00Z', updateDate: null, segment: 'GTM', - isMandatory: false, }, { name: 'Danish', @@ -100,7 +120,6 @@ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => { publishDate: null, updateDate: null, segment: null, - isMandatory: false, }, ], } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.token.ts index 4c1d67688b..e5b2c17c90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/variant-picker/document-variant-picker-modal.token.ts @@ -1,14 +1,14 @@ import { UMB_DOCUMENT_VARIANT_PICKER_MODAL_ALIAS } from '../manifests.js'; -import type { UmbDocumentVariantModel } from '../../types.js'; +import type { UmbDocumentVariantOptionModel } from '../../types.js'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbDocumentVariantPickerModalData { type: 'save' | 'publish' | 'schedule' | 'unpublish'; - variants: Array; + options: Array; } export interface UmbDocumentVariantPickerModalValue { - selection: Array; + selection: Array; } export const UMB_DOCUMENT_LANGUAGE_PICKER_MODAL = new UmbModalToken< diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts index 85526b3f5f..b2e8e57394 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts @@ -46,7 +46,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts index 1ca0862574..7c070f022f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts @@ -1,5 +1,5 @@ import type { UmbDocumentEntityType } from './entity.js'; -import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant'; import type { UmbReferenceById } from '@umbraco-cms/backoffice/models'; import { DocumentVariantStateModel as UmbDocumentVariantState } from '@umbraco-cms/backoffice/external/backend-api'; export { UmbDocumentVariantState }; @@ -22,7 +22,6 @@ export interface UmbDocumentDetailModel { export interface UmbDocumentVariantModel extends UmbVariantModel { state: UmbDocumentVariantState | null; publishDate: string | null; - isMandatory: boolean; } export interface UmbDocumentUrlInfoModel { @@ -36,3 +35,5 @@ export interface UmbDocumentValueModel { alias: string; value: ValueType; } + +export interface UmbDocumentVariantOptionModel extends UmbVariantOptionModel {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-editor.element.ts index f623b6011c..2d7715adba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-editor.element.ts @@ -1,64 +1,36 @@ +import type { UmbDocumentVariantOptionModel } from '../types.js'; import { UmbDocumentWorkspaceSplitViewElement } from './document-workspace-split-view.element.js'; import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from './document-workspace.context-token.js'; import { customElement, state, css, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import type { ActiveVariant } from '@umbraco-cms/backoffice/workspace'; import type { UmbRoute, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; -import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; -import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; -import { combineLatest } from '@umbraco-cms/backoffice/external/rxjs'; +// TODO: This seem fully identical with Media Workspace Editor, so we can refactor this to a generic component. [NL] @customElement('umb-document-workspace-editor') export class UmbDocumentWorkspaceEditorElement extends UmbLitElement { - //private _defaultVariant?: VariantViewModelBaseModel; - - // TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. + // + // TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. [NL] private splitViewElement = new UmbDocumentWorkspaceSplitViewElement(); + #workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; + @state() _routes?: Array; - @state() - _availableVariants: Array = []; - - @state() - _workspaceSplitViews: Array = []; - - #workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; - constructor() { super(); this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (instance) => { this.#workspaceContext = instance; this.#observeVariants(); - this.#observeSplitViews(); }); } #observeVariants() { if (!this.#workspaceContext) return; - this.observe( - this.#workspaceContext.allowedVariants, - (variants) => { - this._availableVariants = variants; - this._generateRoutes(); - }, - '_observeVariants', - ); - } - - #observeSplitViews() { - if (!this.#workspaceContext) return; - this.observe( - this.#workspaceContext.splitView.activeVariantsInfo, - (variants) => { - this._workspaceSplitViews = variants; - }, - '_observeSplitViews', - ); + // TODO: the variantOptions observable is like too broad as this will be triggered then there is any change in the variant options, we need to only update routes when there is a relevant change to them. [NL] + this.observe(this.#workspaceContext.variantOptions, (options) => this._generateRoutes(options), '_observeVariants'); } private _handleVariantFolderPart(index: number, folderPart: string) { @@ -68,17 +40,18 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement { this.#workspaceContext?.splitView.setActiveVariant(index, culture, segment); } - private async _generateRoutes() { - if (!this._availableVariants || this._availableVariants.length === 0) return; + private async _generateRoutes(options: Array) { + if (!options || options.length === 0) return; // Generate split view routes for all available routes const routes: Array = []; // Split view routes: - this._availableVariants.forEach((variantA) => { - this._availableVariants.forEach((variantB) => { + options.forEach((variantA) => { + options.forEach((variantB) => { routes.push({ - path: new UmbVariantId(variantA).toString() + '_&_' + new UmbVariantId(variantB).toString(), + // TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL] + path: variantA.unique + '_&_' + variantB.unique, component: this.splitViewElement, setup: (_component, info) => { // Set split view/active info.. @@ -92,9 +65,10 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement { }); // Single view: - this._availableVariants.forEach((variant) => { + options.forEach((variant) => { routes.push({ - path: new UmbVariantId(variant).toString(), + // TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL] + path: variant.unique, component: this.splitViewElement, setup: (_component, info) => { // cause we might come from a split-view, we need to reset index 1. @@ -108,11 +82,21 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement { // Using first single view as the default route for now (hence the math below): routes.push({ path: '', - redirectTo: routes[this._availableVariants.length * this._availableVariants.length]?.path, + redirectTo: routes[options.length * options.length]?.path, }); } + const oldValue = this._routes; + + // is there any differences in the amount ot the paths? [NL] + // TODO: if we make a memorization function as the observer, we can avoid this check and avoid the whole build of routes. [NL] + if (oldValue && oldValue.length === routes.length) { + // is there any differences in the paths? [NL] + const hasDifferences = oldValue.some((route, index) => route.path !== routes[index].path); + if (!hasDifferences) return; + } this._routes = routes; + this.requestUpdate('_routes', oldValue); } private _gotWorkspaceRoute = (e: UmbRouterSlotInitEvent) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 0c30839953..cc5e6fa680 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -2,12 +2,15 @@ import { UmbDocumentTypeDetailRepository } from '../../document-types/repository import { UmbDocumentPropertyDataContext } from '../property-dataset-context/document-property-dataset-context.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import { UmbDocumentDetailRepository } from '../repository/index.js'; -import { UmbDocumentVariantState, type UmbDocumentDetailModel, type UmbDocumentVariantModel } from '../types.js'; -import type { UmbDocumentVariantPickerModalData } from '../modals/index.js'; +import type { UmbDocumentDetailModel, UmbDocumentVariantModel, UmbDocumentVariantOptionModel } from '../types.js'; +import { UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, type UmbDocumentVariantPickerModalData } from '../modals/index.js'; import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js'; -import { UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT } from '../global-contexts/document-variant-manager.context.js'; import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { + type UmbObjectWithVariantProperties, + UmbVariantId, + variantPropertiesObjectToString, +} from '@umbraco-cms/backoffice/variant'; import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; import { UmbEditableWorkspaceContextBase, @@ -25,11 +28,12 @@ import { import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; type EntityType = UmbDocumentDetailModel; export class UmbDocumentWorkspaceContext extends UmbEditableWorkspaceContextBase - implements UmbVariantableWorkspaceContextInterface, UmbPublishableWorkspaceContextInterface + implements UmbVariantableWorkspaceContextInterface, UmbPublishableWorkspaceContextInterface { // public readonly repository = new UmbDocumentDetailRepository(this); @@ -40,7 +44,7 @@ export class UmbDocumentWorkspaceContext */ #currentData = new UmbObjectState(undefined); #getDataPromise?: Promise; - #variantManagerContext?: typeof UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT.TYPE; + // TODo: Optimize this so it uses either a App Language Context? [NL] #languageRepository = new UmbLanguageCollectionRepository(this); #languages = new UmbArrayState([], (x) => x.unique); public readonly languages = this.#languages.asObservable(); @@ -54,26 +58,18 @@ export class UmbDocumentWorkspaceContext readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.documentType.unique); readonly contentTypeHasCollection = this.#currentData.asObservablePart((data) => !!data?.documentType.collection); readonly variants = this.#currentData.asObservablePart((data) => data?.variants ?? []); - readonly allowedVariants = combineObservables([this.variants, this.languages], ([variants, languages]) => { - const missingLanguages = languages.filter((x) => !variants.some((v) => v.culture === x.unique)); - const newVariants = variants.concat( - missingLanguages.map( - (language) => - ({ - state: UmbDocumentVariantState.NOT_CREATED, - isMandatory: language.isMandatory, - culture: language.unique, - segment: null, - name: language.name, - createDate: null, - publishDate: null, - updateDate: null, - }) as UmbDocumentVariantModel, - ), - ); - return newVariants; + readonly variantOptions = combineObservables([this.variants, this.languages], ([variants, languages]) => { + return languages.map((language) => { + return { + variant: variants.find((x) => x.culture === language.unique), + language, + // TODO: When including segments, this should be updated to include the segment as well. [NL] + unique: language.unique, // This must be a variantId string! + } as UmbDocumentVariantOptionModel; + }); }); - readonly changedVariants = new UmbArrayState([], (x) => x.compare); + + readonly changedVariants = new UmbArrayState([], variantPropertiesObjectToString); readonly urls = this.#currentData.asObservablePart((data) => data?.urls || []); readonly templateId = this.#currentData.asObservablePart((data) => data?.template?.unique || null); @@ -83,16 +79,13 @@ export class UmbDocumentWorkspaceContext constructor(host: UmbControllerHost) { super(host, UMB_DOCUMENT_WORKSPACE_ALIAS); - this.consumeContext(UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT, (instance) => { - this.#variantManagerContext = instance; - }); - this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique)); this.loadLanguages(); } async loadLanguages() { + // TODO: If we don't end up having a Global Context for languages, then we should at least change this into using a asObservable which should be returned from the repository. [Nl] const { data } = await this.#languageRepository.requestCollection({}); this.#languages.setValue(data?.items ?? []); } @@ -222,50 +215,13 @@ export class UmbDocumentWorkspaceContext const data = this.getData(); if (!data) throw new Error('Data is missing'); if (!data.unique) throw new Error('Unique is missing'); - if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); const activeVariants = this.splitView.getActiveVariants(); - const activeVariant = activeVariants.length ? activeVariants[0] : undefined; - // Calculate variants - const pickedVariants: string[] = []; - const availableVariants = await firstValueFrom(this.allowedVariants); - let allowedVariants = data.variants; + const pickedVariants = activeVariants.map((activeVariant) => UmbVariantId.Create(activeVariant).toString()); + const allowedVariants = await firstValueFrom(this.variantOptions); - // Make sure that the active variant is in the allowed variants - if (activeVariant) { - const activeVariantInAvailableVariants = availableVariants.find( - (x) => x.culture === activeVariant.culture && x.segment === activeVariant.segment, - ); - if (activeVariantInAvailableVariants) { - pickedVariants.push(activeVariantInAvailableVariants.culture!); - allowedVariants = appendToFrozenArray( - allowedVariants, - activeVariantInAvailableVariants, - (x) => - x.culture === activeVariantInAvailableVariants.culture && - x.segment === activeVariantInAvailableVariants.segment, - ); - } - } - - // Make sure the changed variants are in the allowed variants - const changedVariants = this.changedVariants.getValue(); - if (changedVariants.length) { - pickedVariants.push(...changedVariants.map((x) => x.culture!)); - const changedVariantsInAvailableVariants = availableVariants.filter((x) => - changedVariants.some((y) => y.equal(new UmbVariantId(x))), - ); - for (const changedVariant of changedVariantsInAvailableVariants) { - allowedVariants = appendToFrozenArray( - allowedVariants, - changedVariant, - (x) => changedVariant.culture === x.culture && changedVariant.segment === x.segment, - ); - } - } - - const selectedVariants = await this.#variantManagerContext.pickVariants(allowedVariants, type, pickedVariants); + const selectedVariants = await this.pickVariants(type, allowedVariants, pickedVariants); // If no variants are selected, we don't save anything. if (!selectedVariants.length) return []; @@ -281,6 +237,35 @@ export class UmbDocumentWorkspaceContext return selectedVariants; } + // TODO: refactor this part so it can be utilized by others? [NL] + async pickVariants( + type: UmbDocumentVariantPickerModalData['type'], + availableVariants: Array, + selectedVariants?: Array, + ): Promise { + // If there is only one variant, we don't need to select anything. + if (availableVariants.length === 1) { + // TODO: we are missing a good way to make a variantId from a variantOptionModel. [NL] + return [new UmbVariantId(availableVariants[0].language.unique, null)]; + } + + const modalManagerContext = await this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, () => {}).asPromise(); + + const modalData: UmbDocumentVariantPickerModalData = { + type, + options: availableVariants, + }; + + const modalContext = modalManagerContext.open(UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, { + data: modalData, + value: { selection: selectedVariants?.map((x) => x.toString()) ?? [] }, + }); + + const result = await modalContext.onSubmit().catch(() => undefined); + + return result?.selection.map((x) => UmbVariantId.FromString(x)) ?? []; + } + async save() { await this.#createOrSave('save'); const data = this.getData(); @@ -304,9 +289,11 @@ export class UmbDocumentWorkspaceContext const unique = this.getEntityId(); if (!unique) throw new Error('Unique is missing'); - if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); + //if (!this.#variantManagerContext) throw new Error('Variant manager context is missing'); - this.#variantManagerContext.unpublish(unique); + //this.#variantManagerContext.unpublish(unique); + alert('not implemented'); + throw new Error('Not implemented'); } async delete() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts index 92427057b9..ad8454fffb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts @@ -1,5 +1,5 @@ import type { UmbMediaEntityType } from './entity.js'; -import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant'; import type { MediaUrlInfoModel, MediaValueModel } from '@umbraco-cms/backoffice/external/backend-api'; export interface UmbMediaDetailModel { @@ -12,3 +12,5 @@ export interface UmbMediaDetailModel { values: Array; variants: Array; } + +export interface UmbMediaVariantOptionModel extends UmbVariantOptionModel {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-editor.element.ts index e5cc052897..f7d8f0e56a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-editor.element.ts @@ -1,28 +1,19 @@ +import type { UmbMediaVariantOptionModel } from '../types.js'; import { UmbMediaWorkspaceSplitViewElement } from './media-workspace-split-view.element.js'; import { UMB_MEDIA_WORKSPACE_CONTEXT } from './media-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { customElement, state, css, html } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbRoute, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; -import type { ActiveVariant } from '@umbraco-cms/backoffice/workspace'; @customElement('umb-media-workspace-editor') export class UmbMediaWorkspaceEditorElement extends UmbLitElement { - //private _defaultVariant?: VariantViewModelBaseModel; - - // TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. + // + // TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. [NL] private splitViewElement = new UmbMediaWorkspaceSplitViewElement(); @state() _routes?: Array; - @state() - _availableVariants: Array = []; - - @state() - _workspaceSplitViews: Array = []; - #workspaceContext?: typeof UMB_MEDIA_WORKSPACE_CONTEXT.TYPE; constructor() { @@ -31,31 +22,12 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement { this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (instance) => { this.#workspaceContext = instance; this.#observeVariants(); - this.#observeSplitViews(); }); } #observeVariants() { if (!this.#workspaceContext) return; - this.observe( - this.#workspaceContext.allowedVariants, - (variants) => { - this._availableVariants = variants; - this._generateRoutes(); - }, - '_observeVariants', - ); - } - - #observeSplitViews() { - if (!this.#workspaceContext) return; - this.observe( - this.#workspaceContext.splitView.activeVariantsInfo, - (variants) => { - this._workspaceSplitViews = variants; - }, - '_observeSplitViews', - ); + this.observe(this.#workspaceContext.variantOptions, (options) => this._generateRoutes(options), '_observeVariants'); } private _handleVariantFolderPart(index: number, folderPart: string) { @@ -65,17 +37,18 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement { this.#workspaceContext?.splitView.setActiveVariant(index, culture, segment); } - private _generateRoutes() { - if (!this._availableVariants || this._availableVariants.length === 0) return; + private async _generateRoutes(variants: Array) { + if (!variants || variants.length === 0) return; // Generate split view routes for all available routes const routes: Array = []; // Split view routes: - this._availableVariants.forEach((variantA) => { - this._availableVariants.forEach((variantB) => { + variants.forEach((variantA) => { + variants.forEach((variantB) => { routes.push({ - path: UmbVariantId.Create(variantA).toString() + '_&_' + UmbVariantId.Create(variantB).toString(), + // TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL] + path: variantA.unique + '_&_' + variantB.unique, component: this.splitViewElement, setup: (_component, info) => { // Set split view/active info.. @@ -89,9 +62,10 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement { }); // Single view: - this._availableVariants.forEach((variant) => { + variants.forEach((variant) => { routes.push({ - path: UmbVariantId.Create(variant).toString(), + // TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL] + path: variant.unique, component: this.splitViewElement, setup: (_component, info) => { // cause we might come from a split-view, we need to reset index 1. @@ -105,11 +79,21 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement { // Using first single view as the default route for now (hence the math below): routes.push({ path: '', - redirectTo: routes[this._availableVariants.length * this._availableVariants.length]?.path, + redirectTo: routes[variants.length * variants.length]?.path, }); } + const oldValue = this._routes; + + // is there any differences in the amount ot the paths? [NL] + // TODO: if we make a memorization function as the observer, we can avoid this check and avoid the whole build of routes. [NL] + if (oldValue && oldValue.length === routes.length) { + // is there any differences in the paths? [NL] + const hasDifferences = oldValue.some((route, index) => route.path !== routes[index].path); + if (!hasDifferences) return; + } this._routes = routes; + this.requestUpdate('_routes', oldValue); } private _gotWorkspaceRoute = (e: UmbRouterSlotInitEvent) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index f65aa898f2..fa42e589d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -2,7 +2,7 @@ import { UmbMediaTypeDetailRepository } from '../../media-types/repository/detai import { UmbMediaPropertyDataContext } from '../property-dataset-context/media-property-dataset-context.js'; import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; import { UmbMediaDetailRepository } from '../repository/index.js'; -import type { UmbMediaDetailModel } from '../types.js'; +import type { UmbMediaDetailModel, UmbMediaVariantOptionModel } from '../types.js'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; import { @@ -33,8 +33,11 @@ export class UmbMediaWorkspaceContext */ #currentData = new UmbObjectState(undefined); #getDataPromise?: Promise; + // TODo: Optimize this so it uses either a App Language Context? [NL] #languageRepository = new UmbLanguageCollectionRepository(this); - #languageCollection = new UmbArrayState([], (x) => x.unique); + #languages = new UmbArrayState([], (x) => x.unique); + public readonly languages = this.#languages.asObservable(); + public isLoaded() { return this.#getDataPromise; } @@ -43,22 +46,16 @@ export class UmbMediaWorkspaceContext readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.mediaType.unique); readonly variants = this.#currentData.asObservablePart((data) => data?.variants || []); - readonly allowedVariants = combineObservables( - [this.variants, this.#languageCollection.asObservable()], - ([variants, languages]) => { - const missingLanguages = languages.filter((x) => !variants.some((v) => v.culture === x.unique)); - const newVariants = variants.concat( - missingLanguages.map((x) => ({ - culture: x.unique, - segment: null, - name: x.name, - createDate: '', - updateDate: '', - })), - ); - return newVariants; - }, - ); + readonly variantOptions = combineObservables([this.variants, this.languages], ([variants, languages]) => { + return languages.map((language) => { + return { + variant: variants.find((x) => x.culture === language.unique), + language, + // TODO: When including segments, this should be updated to include the segment as well. [NL] + unique: language.unique, // This must be a variantId string! + } as UmbMediaVariantOptionModel; + }); + }); readonly urls = this.#currentData.asObservablePart((data) => data?.urls || []); readonly structure = new UmbContentTypePropertyStructureManager(this, new UmbMediaTypeDetailRepository(this)); @@ -73,7 +70,7 @@ export class UmbMediaWorkspaceContext async loadLanguages() { const { data } = await this.#languageRepository.requestCollection({}); - this.#languageCollection.setValue(data?.items ?? []); + this.#languages.setValue(data?.items ?? []); } async load(unique: string) {