diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 2eb75aa3993f..137b1f1e2a2e 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -57,6 +57,7 @@ "./icon": "./dist-cms/packages/core/icon-registry/index.js", "./id": "./dist-cms/packages/core/id/index.js", "./imaging": "./dist-cms/packages/media/imaging/index.js", + "./interaction-memory": "./dist-cms/packages/core/interaction-memory/index.js", "./language": "./dist-cms/packages/language/index.js", "./lit-element": "./dist-cms/packages/core/lit-element/index.js", "./localization": "./dist-cms/packages/core/localization/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts index d1236bc97bef..89763284e8c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts @@ -1,10 +1,12 @@ +import { manifests as coreManifests } from './manifests.js'; import { UMB_AUTH_CONTEXT } from './auth/auth.context.token.js'; -import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerElement } from './components/index.js'; import { UmbActionEventContext } from './action/action-event.context.js'; -import { manifests as coreManifests } from './manifests.js'; -import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; +import { UmbBackofficeNotificationContainerElement, UmbBackofficeModalContainerElement } from './components/index.js'; +import { UmbInteractionMemoryContext } from './interaction-memory/index.js'; +import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; -import { UmbExtensionsApiInitializer, type UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api'; +import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; +import type { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api'; import './property-action/components/index.js'; import './menu/components/index.js'; @@ -31,6 +33,7 @@ export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => { new UmbNotificationContext(host); new UmbModalManagerContext(host); new UmbActionEventContext(host); + new UmbInteractionMemoryContext(host); host.consumeContext(UMB_AUTH_CONTEXT, (authContext) => { // Initialize the auth context to let the app context know that the core module is ready diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/constants.ts new file mode 100644 index 000000000000..9641d5013b62 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/constants.ts @@ -0,0 +1 @@ +export * from './interaction-memory.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/event/interaction-memories-change.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/event/interaction-memories-change.event.ts new file mode 100644 index 000000000000..e9d65ff06538 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/event/interaction-memories-change.event.ts @@ -0,0 +1,8 @@ +export class UmbInteractionMemoriesChangeEvent extends Event { + public static readonly TYPE = 'interaction-memories-change'; + + public constructor() { + // mimics the native change event + super(UmbInteractionMemoriesChangeEvent.TYPE, { bubbles: true, composed: false, cancelable: false }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/index.ts new file mode 100644 index 000000000000..175b537a068f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/index.ts @@ -0,0 +1,6 @@ +export * from './constants.js'; +export * from './event/interaction-memories-change.event.js'; +export * from './interaction-memory.context.js'; +export * from './interaction-memory.manager.js'; + +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.context.token.ts new file mode 100644 index 000000000000..d37a64fe99f0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbInteractionMemoryContext } from './interaction-memory.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_INTERACTION_MEMORY_CONTEXT = new UmbContextToken( + 'UmbInteractionMemoryContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.context.ts new file mode 100644 index 000000000000..32e267129af4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.context.ts @@ -0,0 +1,12 @@ +import { UMB_INTERACTION_MEMORY_CONTEXT } from './interaction-memory.context.token.js'; +import { UmbInteractionMemoryManager } from './interaction-memory.manager.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbInteractionMemoryContext extends UmbContextBase { + public readonly memory = new UmbInteractionMemoryManager(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_INTERACTION_MEMORY_CONTEXT); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.manager.test.ts new file mode 100644 index 000000000000..6c506732d6ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.manager.test.ts @@ -0,0 +1,103 @@ +import { UmbInteractionMemoryManager } from './interaction-memory.manager.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { expect } from '@open-wc/testing'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; + +@customElement('test-my-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +describe('UmbInteractionMemoryManager', () => { + let manager: UmbInteractionMemoryManager; + const nestedMemory1 = { unique: 'nestedMemory1', value: 'Nested Memory 1' }; + const nestedMemory2 = { unique: 'nestedMemory2', value: 'Nested Memory 2' }; + const memory1 = { unique: '1', value: 'Memory 1' }; + const memory2 = { unique: '2', value: 'Memory 2', memories: [nestedMemory1, nestedMemory2] }; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + manager = new UmbInteractionMemoryManager(hostElement); + manager.setMemory(memory1); + manager.setMemory(memory2); + }); + + describe('Public API', () => { + describe('properties', () => { + it('has a memories property', () => { + expect(manager).to.have.property('memories').to.be.an.instanceOf(Observable); + }); + }); + + describe('methods', () => { + it('has a memory method', () => { + expect(manager).to.have.property('memory').that.is.a('function'); + }); + + it('has a getMemory method', () => { + expect(manager).to.have.property('getMemory').that.is.a('function'); + }); + + it('has a setMemory method', () => { + expect(manager).to.have.property('setMemory').that.is.a('function'); + }); + + it('has a deleteMemory method', () => { + expect(manager).to.have.property('deleteMemory').that.is.a('function'); + }); + + it('has a getAllMemories method', () => { + expect(manager).to.have.property('getAllMemories').that.is.a('function'); + }); + + it('has a clear method', () => { + expect(manager).to.have.property('clear').that.is.a('function'); + }); + }); + }); + + describe('getMemory()', () => { + it('returns the correct memory item by unique identifier', () => { + const result = manager.getMemory('1'); + expect(result).to.deep.equal(memory1); + }); + }); + + describe('setMemory()', () => { + it('create a new memory unique identifier', () => { + const newMemory = { unique: 'newMemory', value: 'New Memory' }; + manager.setMemory(newMemory); + const result = manager.getMemory('newMemory'); + expect(result).to.deep.equal(newMemory); + }); + + it('update an existing memory item by unique identifier', () => { + const updatedMemory = { unique: '1', value: 'Updated Memory 1' }; + manager.setMemory(updatedMemory); + const result = manager.getMemory('1'); + expect(result).to.deep.equal(updatedMemory); + }); + }); + + describe('deleteMemory()', () => { + it('deletes an existing memory item by unique identifier', () => { + manager.deleteMemory('1'); + const result = manager.getMemory('1'); + expect(result).to.be.undefined; + }); + }); + + describe('getAllMemories()', () => { + it('returns all memory items', () => { + const result = manager.getAllMemories(); + expect(result).to.deep.equal([memory1, memory2]); + }); + }); + + describe('clear()', () => { + it('clears all memory items', () => { + manager.clear(); + const result = manager.getAllMemories(); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.manager.ts new file mode 100644 index 000000000000..a385e4a59c49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/interaction-memory.manager.ts @@ -0,0 +1,71 @@ +import type { UmbInteractionMemoryModel } from './types.js'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; + +/** + * A manager for handling interaction memory items. + * @exports + * @class UmbInteractionMemoryManager + * @augments {UmbControllerBase} + */ +export class UmbInteractionMemoryManager extends UmbControllerBase { + #memories = new UmbArrayState([], (x) => x.unique); + /** Observable for all memory items. */ + memories = this.#memories.asObservable(); + + /** + * Observable for a specific memory item by its unique identifier. + * @param {string} unique - The unique identifier of the memory item. + * @returns {(Observable)} An observable that emits the memory item or undefined if not found. + * @memberof UmbInteractionMemoryManager + */ + memory(unique: string): Observable { + return this.#memories.asObservablePart((items) => items.find((item) => item.unique === unique)); + } + + /** + * Get a specific memory item by its unique identifier. + * @param {string} unique - The unique identifier of the memory item. + * @returns {(UmbInteractionMemoryModel | undefined)} The memory item or undefined if not found. + * @memberof UmbInteractionMemoryManager + */ + getMemory(unique: string): UmbInteractionMemoryModel | undefined { + return this.#memories.getValue().find((item) => item.unique === unique); + } + + /** + * Add or update a memory item. + * @param {UmbInteractionMemoryModel} memory - The memory item to add or update. + * @memberof UmbInteractionMemoryManager + */ + setMemory(memory: UmbInteractionMemoryModel) { + this.#memories.appendOne(memory); + } + + /** + * Delete a memory item by its unique identifier. + * @param {string} unique - The unique identifier of the memory item. + * @memberof UmbInteractionMemoryManager + */ + deleteMemory(unique: string) { + this.#memories.removeOne(unique); + } + + /** + * Get all memory items from the manager. + * @returns {Array} An array of all memory items. + * @memberof UmbInteractionMemoryManager + */ + getAllMemories(): Array { + return this.#memories.getValue(); + } + + /** + * Clear all memory items from the manager. + * @memberof UmbInteractionMemoryManager + */ + clear() { + this.#memories.clear(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/types.ts new file mode 100644 index 000000000000..7f3d18c5314c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/interaction-memory/types.ts @@ -0,0 +1,5 @@ +export interface UmbInteractionMemoryModel { + unique: string; + value?: any; + memories?: Array; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts index 3271d035d437..b27388d406e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/context/modal.context.ts @@ -2,12 +2,14 @@ import { UmbModalToken } from '../token/modal-token.js'; import type { UmbModalConfig, UmbModalType } from '../types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; +import { umbDeepMerge } from '@umbraco-cms/backoffice/utils'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import { type UmbDeepPartialObject, umbDeepMerge } from '@umbraco-cms/backoffice/utils'; +import { UMB_ROUTE_CONTEXT } from '@umbraco-cms/backoffice/router'; import type { ElementLoaderProperty } from '@umbraco-cms/backoffice/extension-api'; -import { UMB_ROUTE_CONTEXT, type IRouterSlot } from '@umbraco-cms/backoffice/router'; +import type { IRouterSlot } from '@umbraco-cms/backoffice/router'; +import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils'; export interface UmbModalRejectReason { type: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts index 97b3ebb81ed2..3fe706855006 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts @@ -1,5 +1,5 @@ -import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; import type { ElementLoaderProperty } from '@umbraco-cms/backoffice/extension-api'; +import type { UUIModalElement, UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui'; export type * from './extensions/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index c6407d1c5498..11d3bdeeb221 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -1,12 +1,18 @@ import { UMB_PICKER_INPUT_CONTEXT } from './picker-input.context-token.js'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +import { UmbInteractionMemoryManager } from '@umbraco-cms/backoffice/interaction-memory'; import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; -import { umbConfirmModal, umbOpenModal } from '@umbraco-cms/backoffice/modal'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; -import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; -import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +import { + umbConfirmModal, + umbOpenModal, + type UmbModalToken, + type UmbPickerModalData, + type UmbPickerModalValue, +} from '@umbraco-cms/backoffice/modal'; type PickerItemBaseType = { name: string; unique: string }; export class UmbPickerInputContext< @@ -21,8 +27,9 @@ export class UmbPickerInputContext< #itemManager; - selection; - selectedItems; + public readonly selection; + public readonly selectedItems; + public readonly interactionMemory = new UmbInteractionMemoryManager(this); /** * Define a minimum amount of selected items in this input, for this input to be valid. @@ -100,6 +107,7 @@ export class UmbPickerInputContext< selection: this.getSelection(), } as PickerModalValueType, }).catch(() => undefined); + if (!modalValue) return; this.setSelection(modalValue.selection); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/index.ts index d38d1310d8e2..fb3be7e1556f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/index.ts @@ -1,5 +1,6 @@ export * from './constants.js'; -export * from './search/index.js'; +export * from './modal/index.js'; export * from './picker.context.js'; export * from './picker.context.token.js'; +export * from './search/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/modal/index.ts new file mode 100644 index 000000000000..762dc5e74cf6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/modal/index.ts @@ -0,0 +1 @@ +export * from './picker-modal-base.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/modal/picker-modal-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/modal/picker-modal-base.element.ts new file mode 100644 index 000000000000..85aa28e4807a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/modal/picker-modal-base.element.ts @@ -0,0 +1,64 @@ +import type { UmbPickerContext } from '../picker.context.js'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; +import type { ManifestModal, UmbPickerModalData } from '@umbraco-cms/backoffice/modal'; +import { UMB_PICKER_INPUT_CONTEXT } from '@umbraco-cms/backoffice/picker-input'; + +export abstract class UmbPickerModalBaseElement< + ItemType = UmbEntityModel, + ModalDataType extends UmbPickerModalData = UmbPickerModalData, + ModalValueType = unknown, + ModalManifestType extends ManifestModal = ManifestModal, +> extends UmbModalBaseElement { + protected abstract _pickerContext: UmbPickerContext; + + #pickerInputContext?: typeof UMB_PICKER_INPUT_CONTEXT.TYPE; + + constructor() { + super(); + this.consumeContext(UMB_PICKER_INPUT_CONTEXT, (pickerInputContext) => { + this.#pickerInputContext = pickerInputContext; + this.#observeMemoriesFromInputContext(); + }); + } + + override connectedCallback(): void { + super.connectedCallback(); + this.#observeMemoriesFromPicker(); + } + + #observeMemoriesFromPicker() { + this.observe(this._pickerContext.interactionMemory.memories, (memories) => { + this.#setMemoriesOnInputContext(memories); + }); + } + + #getInteractionMemoryUnique() { + // TODO: consider appending with a unique when we have that implemented. + return `UmbPickerModal`; + } + + #observeMemoriesFromInputContext() { + this.observe( + this.#pickerInputContext?.interactionMemory.memory(this.#getInteractionMemoryUnique()), + (memory) => { + memory?.memories?.forEach((memory) => this._pickerContext.interactionMemory.setMemory(memory)); + }, + 'umbModalInteractionMemoryObserver', + ); + } + + #setMemoriesOnInputContext(pickerMemories: Array) { + if (pickerMemories?.length > 0) { + const pickerModalMemory: UmbInteractionMemoryModel = { + unique: this.#getInteractionMemoryUnique(), + memories: pickerMemories, + }; + + this.#pickerInputContext?.interactionMemory.setMemory(pickerModalMemory); + } else { + this.#pickerInputContext?.interactionMemory.deleteMemory(this.#getInteractionMemoryUnique()); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.ts index e606e79b0ffc..63a16f123ad4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.ts @@ -1,18 +1,23 @@ import { UMB_PICKER_CONTEXT } from './picker.context.token.js'; import { UmbPickerSearchManager } from './search/manager/picker-search.manager.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbInteractionMemoryManager } from '@umbraco-cms/backoffice/interaction-memory'; +import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; import { UMB_PROPERTY_TYPE_BASED_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; export class UmbPickerContext extends UmbContextBase { + public readonly interactionMemory = new UmbInteractionMemoryManager(this); public readonly selection = new UmbSelectionManager(this); public readonly search = new UmbPickerSearchManager(this); + public dataType?: { unique: string }; constructor(host: UmbControllerHost) { super(host, UMB_PICKER_CONTEXT); + /* TODO: Move this implementation to another place. The generic picker context shouldn't be aware of property and data types. + It also gives an illegal import of content module */ this.consumeContext(UMB_PROPERTY_TYPE_BASED_PROPERTY_CONTEXT, (context) => { this.observe(context?.dataType, (dataType) => { this.dataType = dataType; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts index 9d73c0fdba2a..35a44053645d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts @@ -1,10 +1,9 @@ import type { UmbPickerSearchManagerConfig } from './types.js'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { debounce } from '@umbraco-cms/backoffice/utils'; import { UmbArrayState, UmbBooleanState, UmbNumberState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbSearchProvider, UmbSearchRequestArgs, UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search'; -import { debounce } from '@umbraco-cms/backoffice/utils'; /** * A manager for searching items in a picker. @@ -36,15 +35,6 @@ export class UmbPickerSearchManager< #config?: UmbPickerSearchManagerConfig; #searchProvider?: UmbSearchProvider; - /** - * Creates an instance of UmbPickerSearchManager. - * @param {UmbControllerHost} host The controller host for the search manager. - * @memberof UmbPickerSearchManager - */ - constructor(host: UmbControllerHost) { - super(host); - } - /** * Set the configuration for the search manager. * @param {UmbPickerSearchManagerConfig} config The configuration for the search manager. @@ -187,6 +177,7 @@ export class UmbPickerSearchManager< // ensure that config params are always included ...this.#config?.queryParams, searchFrom: this.#config?.searchFrom, + // TODO: Move this implementation to another place. The generic picker search manager shouldn't be aware of data types. dataTypeUnique: this.#config?.dataTypeUnique, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts index 6f36bd603767..016483412b66 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/index.ts @@ -2,5 +2,6 @@ export * from './components/index.js'; export * from './config/index.js'; export * from './constants.js'; export * from './events/index.js'; +export * from './interaction-memory/index.js'; export * from './ui-picker-modal/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/index.ts new file mode 100644 index 000000000000..835e4df1982b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/index.ts @@ -0,0 +1 @@ +export * from './property-editor-ui-interaction-memory.manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/property-editor-ui-interaction-memory.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/property-editor-ui-interaction-memory.manager.test.ts new file mode 100644 index 000000000000..4f775c857837 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/property-editor-ui-interaction-memory.manager.test.ts @@ -0,0 +1,128 @@ +import { UmbPropertyEditorUiInteractionMemoryManager } from './property-editor-ui-interaction-memory.manager.js'; +import { UmbPropertyEditorConfigCollection } from '../config/index.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { expect } from '@open-wc/testing'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { UmbInteractionMemoryContext } from '@umbraco-cms/backoffice/interaction-memory'; + +@customElement('test-my-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) { + constructor() { + super(); + new UmbInteractionMemoryContext(this); + } +} + +describe('UmbPropertyEditorUiInteractionMemoryManager', () => { + let manager: UmbPropertyEditorUiInteractionMemoryManager; + let childMemories = [ + { unique: '1', value: 'Value 1' }, + { unique: '2', value: 'Value 2' }, + ]; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + document.body.appendChild(hostElement); + + manager = new UmbPropertyEditorUiInteractionMemoryManager(hostElement, { + memoryUniquePrefix: 'TestPrefix', + }); + + // A random config to generate a hash code from + const config = new UmbPropertyEditorConfigCollection([ + { + alias: 'someAlias', + value: 'someValue', + }, + ]); + + manager.setPropertyEditorConfig(config); + }); + + describe('Public API', () => { + describe('properties', () => { + it('has a memoriesForPropertyEditor property', () => { + expect(manager).to.have.property('memoriesForPropertyEditor').to.be.an.instanceOf(Observable); + }); + }); + + describe('methods', () => { + it('has a setPropertyEditorConfig method', () => { + expect(manager).to.have.property('setPropertyEditorConfig').that.is.a('function'); + }); + + it('has a saveMemoriesForPropertyEditor method', () => { + expect(manager).to.have.property('saveMemoriesForPropertyEditor').that.is.a('function'); + }); + + it('has a deleteMemoriesForPropertyEditor method', () => { + expect(manager).to.have.property('deleteMemoriesForPropertyEditor').that.is.a('function'); + }); + }); + + describe('saveMemoriesForPropertyEditor', () => { + it('creates a property editor memory based on the provided data', (done) => { + manager.memoriesForPropertyEditor.subscribe((memories) => { + if (memories.length > 0) { + expect(memories).to.have.lengthOf(2); + expect(memories).to.deep.equal(childMemories); + done(); + } + }); + + manager.saveMemoriesForPropertyEditor(childMemories); + }); + + it('updates the property editor memory based on the provided data', (done) => { + const updatedChildMemories = [ + { unique: '1', value: 'Updated Value 1' }, + { unique: '2', value: 'Updated Value 2' }, + { unique: '3', value: 'New Value 3' }, + ]; + + // We start at -1 because the first call is the initial empty array + let callCount = -1; + manager.memoriesForPropertyEditor.subscribe((memories) => { + callCount++; + if (callCount === 1) { + // First call, after initial save + expect(memories).to.have.lengthOf(2); + expect(memories).to.deep.equal(childMemories); + } else if (callCount === 2) { + // Second call, after update + expect(memories).to.have.lengthOf(3); + expect(memories).to.deep.equal(updatedChildMemories); + done(); + } + }); + + manager.saveMemoriesForPropertyEditor(childMemories); + manager.saveMemoriesForPropertyEditor(updatedChildMemories); + }); + }); + + describe('deleteMemoriesForPropertyEditor', () => { + it('deletes all memories for this property editor', (done) => { + // We start at -1 because the first call is the initial empty array + let callCount = -1; + manager.memoriesForPropertyEditor.subscribe((memories) => { + callCount++; + if (callCount === 1) { + // First call, after initial save + expect(memories).to.have.lengthOf(2); + expect(memories).to.deep.equal(childMemories); + } else if (callCount === 2) { + // Second call, after delete + expect(memories).to.have.lengthOf(0); + expect(memories).to.deep.equal([]); + done(); + } + }); + + manager.saveMemoriesForPropertyEditor(childMemories); + manager.deleteMemoriesForPropertyEditor(); + }); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/property-editor-ui-interaction-memory.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/property-editor-ui-interaction-memory.manager.ts new file mode 100644 index 000000000000..8d86e21b767e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/interaction-memory/property-editor-ui-interaction-memory.manager.ts @@ -0,0 +1,93 @@ +import type { UmbPropertyEditorConfigCollection } from '../config/index.js'; +import { simpleHashCode, UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_INTERACTION_MEMORY_CONTEXT } from '@umbraco-cms/backoffice/interaction-memory'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; + +export interface UmbPropertyEditorUiInteractionMemoryManagerArgs { + memoryUniquePrefix: string; +} + +export class UmbPropertyEditorUiInteractionMemoryManager extends UmbControllerBase { + #memories = new UmbArrayState([], (x) => x.unique); + memoriesForPropertyEditor = this.#memories.asObservable(); + + #interactionMemoryContext?: typeof UMB_INTERACTION_MEMORY_CONTEXT.TYPE; + #configHashCode?: number; + #memoryUniquePrefix: string; + #init?: Promise; + + constructor(host: UmbControllerHost, args: UmbPropertyEditorUiInteractionMemoryManagerArgs) { + super(host); + + this.#memoryUniquePrefix = args.memoryUniquePrefix; + + this.#init = Promise.all([ + this.consumeContext(UMB_INTERACTION_MEMORY_CONTEXT, (context) => { + this.#interactionMemoryContext = context; + }).asPromise(), + ]); + } + + /** + * Sets the property editor config, used to create a unique hash for the interaction memory. + * @param {(UmbPropertyEditorConfigCollection | undefined)} config + * @memberof UmbPropertyEditorUiInteractionMemoryManager + */ + setPropertyEditorConfig(config: UmbPropertyEditorConfigCollection | undefined) { + this.#setConfigHash(config); + this.#getInteractionMemory(); + } + + /** + * Creates or updates an interaction memory for this property editor based on the provided memories. + * @param {Array} memories - The memories to include for this property editor. + * @returns {Promise} + * @memberof UmbPropertyEditorUiInteractionMemoryManager + */ + async saveMemoriesForPropertyEditor(memories: Array): Promise { + await this.#init; + const memoryUnique = this.#getInteractionMemoryUnique(); + if (!this.#interactionMemoryContext) return; + + const propertyEditorMemory: UmbInteractionMemoryModel = { + unique: memoryUnique, + memories, + }; + + this.#interactionMemoryContext.memory.setMemory(propertyEditorMemory); + this.#memories.setValue(memories); + } + + /** + * Deletes the interaction memory for this property editor. + * @memberof UmbPropertyEditorUiInteractionMemoryManager + */ + async deleteMemoriesForPropertyEditor(): Promise { + await this.#init; + const unique = this.#getInteractionMemoryUnique(); + this.#interactionMemoryContext?.memory.deleteMemory(unique); + this.#memories.setValue([]); + } + + #getInteractionMemoryUnique() { + return `${this.#memoryUniquePrefix}PropertyEditorUi${this.#configHashCode ? '-' + this.#configHashCode : ''}`; + } + + async #getInteractionMemory() { + await this.#init; + const memoryUnique = this.#getInteractionMemoryUnique(); + if (!memoryUnique) return; + if (!this.#interactionMemoryContext) return; + + const memory = this.#interactionMemoryContext.memory.getMemory(memoryUnique); + this.#memories.setValue(memory?.memories ?? []); + } + + #setConfigHash(config: UmbPropertyEditorConfigCollection | undefined) { + const configString = config ? JSON.stringify(config.toObject()) : ''; + const hashCode = simpleHashCode(configString); + this.#configHashCode = hashCode; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker-expansion.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker-expansion.manager.ts new file mode 100644 index 000000000000..7d4d3f7070e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker-expansion.manager.ts @@ -0,0 +1,94 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbEntityExpansionManager } from '@umbraco-cms/backoffice/utils'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbEntityExpansionModel } from '@umbraco-cms/backoffice/utils'; +import type { + UmbInteractionMemoryManager, + UmbInteractionMemoryModel, +} from '@umbraco-cms/backoffice/interaction-memory'; + +export interface UmbTreeItemPickerExpansionManagerArgs { + interactionMemoryManager?: UmbInteractionMemoryManager; +} + +export class UmbTreeItemPickerExpansionManager extends UmbControllerBase { + #manager = new UmbEntityExpansionManager(this); + public readonly expansion = this.#manager.expansion; + + #interactionMemoryManager?: UmbInteractionMemoryManager; + #interactionMemoryUnique: string = 'UmbTreeItemPickerExpansion'; + #muteMemoryObservation = false; + + constructor(host: UmbControllerHost, args?: UmbTreeItemPickerExpansionManagerArgs) { + super(host); + this.#interactionMemoryManager = args?.interactionMemoryManager; + + if (this.#interactionMemoryManager) { + this.#observeInteractionMemory(); + } + } + + /** + * Sets the full expansion state + * @param {UmbEntityExpansionModel} expansion - The full expansion state to set + * @memberof UmbTreeItemPickerExpansionManager + */ + setExpansion(expansion: UmbEntityExpansionModel): void { + this.#manager.setExpansion(expansion); + + // Store the latest expansion state in interaction memory + if (expansion.length > 0) { + this.#setExpansionMemory(); + } else { + this.#removeExpansionMemory(); + } + } + + /** + * Gets the current expansion state + * @returns {UmbEntityExpansionModel} The full expansion state + * @memberof UmbTreeItemPickerExpansionManager + */ + getExpansion(): UmbEntityExpansionModel { + return this.#manager.getExpansion(); + } + + #observeInteractionMemory() { + this.observe(this.#interactionMemoryManager?.memory(this.#interactionMemoryUnique), (memory) => { + if (this.#muteMemoryObservation) return; + + if (memory) { + this.#applyExpansionInteractionMemory(memory); + } + }); + } + + #setExpansionMemory() { + if (!this.#interactionMemoryManager) return; + + // Add a memory entry with the latest expansion state + const memory: UmbInteractionMemoryModel = { + unique: this.#interactionMemoryUnique, + value: { + expansion: this.getExpansion(), + }, + }; + + this.#muteMemoryObservation = true; + this.#interactionMemoryManager?.setMemory(memory); + this.#muteMemoryObservation = false; + } + + #removeExpansionMemory() { + if (!this.#interactionMemoryManager) return; + this.#interactionMemoryManager.deleteMemory(this.#interactionMemoryUnique); + } + + #applyExpansionInteractionMemory(memory: UmbInteractionMemoryModel) { + const memoryExpansion = memory?.value?.expansion as UmbEntityExpansionModel | undefined; + + if (memoryExpansion) { + this.#manager.setExpansion(memoryExpansion); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker.context.ts index 99f163c25cb4..bd30dcf6385b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item-picker/tree-item-picker.context.ts @@ -1,10 +1,10 @@ +import { UmbTreeItemPickerExpansionManager } from './tree-item-picker-expansion.manager.js'; import { UmbPickerContext } from '@umbraco-cms/backoffice/picker'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbTreeItemPickerContext extends UmbPickerContext { - constructor(host: UmbControllerHost) { - super(host); - } + public readonly expansion = new UmbTreeItemPickerExpansionManager(this, { + interactionMemoryManager: this.interactionMemory, + }); } export { UmbTreeItemPickerContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts index bbd95cd19c6e..fb71dd349de7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts @@ -1,15 +1,18 @@ -import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '../types.js'; import { UmbTreeItemPickerContext } from '../tree-item-picker/index.js'; +import type { UmbTreeElement } from '../tree.element.js'; +import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '../types.js'; import type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker-modal.token.js'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; -import { html, customElement, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; +import { customElement, html, ifDefined, nothing, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; +import { UmbPickerModalBaseElement } from '@umbraco-cms/backoffice/picker'; +import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbEntityExpansionModel, UmbExpansionChangeEvent } from '@umbraco-cms/backoffice/utils'; @customElement('umb-tree-picker-modal') -export class UmbTreePickerModalElement extends UmbModalBaseElement< +export class UmbTreePickerModalElement extends UmbPickerModalBaseElement< + TreeItemType, UmbTreePickerModalData, UmbTreePickerModalValue > { @@ -32,16 +35,20 @@ export class UmbTreePickerModalElement { + this._pickerContext.selection.setSelectable(true); + this.observe(this._pickerContext.selection.hasSelection, (hasSelection) => { this._hasSelection = hasSelection; }); this.#observePickerSelection(); this.#observeSearch(); + this.#observeExpansion(); } override connectedCallback(): void { @@ -54,15 +61,15 @@ export class UmbTreePickerModalElement { this.updateValue({ selection }); this.requestUpdate(); @@ -93,7 +100,7 @@ export class UmbTreePickerModalElement { this._searchQuery = query?.query; }, @@ -101,16 +108,26 @@ export class UmbTreePickerModalElement { + this._treeExpansion = value; + }, + 'umbTreeItemPickerExpansionObserver', + ); + } + // Tree Selection #onTreeItemSelected(event: UmbSelectedEvent) { event.stopPropagation(); - this.#pickerContext.selection.select(event.unique); + this._pickerContext.selection.select(event.unique); this.modalContext?.dispatchEvent(new UmbSelectedEvent(event.unique)); } #onTreeItemDeselected(event: UmbDeselectedEvent) { event.stopPropagation(); - this.#pickerContext.selection.deselect(event.unique); + this._pickerContext.selection.deselect(event.unique); this.modalContext?.dispatchEvent(new UmbDeselectedEvent(event.unique)); } @@ -149,6 +166,12 @@ export class UmbTreePickerModalElement @@ -181,9 +204,11 @@ export class UmbTreePickerModalElement + @deselected=${this.#onTreeItemDeselected} + @expansion-change=${this.#onTreeItemExpansionChange}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts index 6df35d3cd1ad..35aa800b95c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts @@ -37,6 +37,7 @@ export default defineConfig({ 'id/index': './id/index.ts', 'lit-element/index': './lit-element/index.ts', 'localization/index': './localization/index.ts', + 'interaction-memory/index': './interaction-memory/index.ts', 'menu/index': './menu/index.ts', 'modal/index': './modal/index.ts', 'models/index': './models/index.ts', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 3767445c56fa..4e848125fbed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -1,13 +1,16 @@ import type { UmbDocumentItemModel } from '../../item/types.js'; import { UmbDocumentPickerInputContext } from './input-document.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { jsonStringComparison } from '@umbraco-cms/backoffice/observable-api'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UmbInteractionMemoriesChangeEvent } from '@umbraco-cms/backoffice/interaction-memory'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; -import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document-type'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; @customElement('umb-input-document') export class UmbInputDocumentElement extends UmbFormControlMixin( @@ -37,10 +40,10 @@ export class UmbInputDocumentElement extends UmbFormControlMixin) { - this.#pickerContext.setSelection(ids); + this.#pickerInputContext.setSelection(ids); this.#sorter.setModel(ids); } public get selection(): Array { - return this.#pickerContext.getSelection(); + return this.#pickerInputContext.getSelection(); } @property({ type: Object, attribute: false }) @@ -122,10 +125,21 @@ export class UmbInputDocumentElement extends UmbFormControlMixin | undefined { + return this.#pickerInputContext.interactionMemory.getAllMemories(); + } + public set interactionMemories(value: Array | undefined) { + this.#interactionMemories = value; + value?.forEach((memory) => this.#pickerInputContext.interactionMemory.setMemory(memory)); + } + + #interactionMemories?: Array = []; + @state() private _items?: Array; - #pickerContext = new UmbDocumentPickerInputContext(this); + #pickerInputContext = new UmbDocumentPickerInputContext(this); constructor() { super(); @@ -142,12 +156,35 @@ export class UmbInputDocumentElement extends UmbFormControlMixin !!this.max && this.selection.length > this.max, ); - this.observe(this.#pickerContext.selection, (selection) => (this.value = selection.join(',')), '_observeSelection'); - this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems), '_observerItems'); + this.observe( + this.#pickerInputContext.selection, + (selection) => (this.value = selection.join(',')), + '_observeSelection', + ); + + this.observe( + this.#pickerInputContext.selectedItems, + (selectedItems) => (this._items = selectedItems), + '_observerItems', + ); + + this.observe( + this.#pickerInputContext.interactionMemory.memories, + (memories) => { + // only dispatch the event if the interaction memories have actually changed + const isIdentical = jsonStringComparison(memories, this.#interactionMemories); + + if (!isIdentical) { + this.#interactionMemories = memories; + this.dispatchEvent(new UmbInteractionMemoriesChangeEvent()); + } + }, + '_observeMemories', + ); } #openPicker() { - this.#pickerContext.openPicker( + this.#pickerInputContext.openPicker( { hideTreeRoot: true, startNode: this.startNode, @@ -163,7 +200,7 @@ export class UmbInputDocumentElement extends UmbFormControlMixin('validationLimit'); @@ -47,11 +51,37 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl @state() private _startNodeId?: string; + @state() + private _interactionMemories: Array = []; + + #interactionMemoryManager = new UmbPropertyEditorUiInteractionMemoryManager(this, { + memoryUniquePrefix: 'UmbDocumentPicker', + }); + + constructor() { + super(); + + this.observe(this.#interactionMemoryManager.memoriesForPropertyEditor, (interactionMemories) => { + this._interactionMemories = interactionMemories ?? []; + }); + } + #onChange(event: CustomEvent & { target: UmbInputDocumentElement }) { this.value = event.target.value; this.dispatchEvent(new UmbChangeEvent()); } + async #onInputInteractionMemoriesChange(event: UmbChangeEvent) { + const target = event.target as UmbInputDocumentElement; + const interactionMemories = target.interactionMemories; + + if (interactionMemories && interactionMemories.length > 0) { + await this.#interactionMemoryManager.saveMemoriesForPropertyEditor(interactionMemories); + } else { + await this.#interactionMemoryManager.deleteMemoriesForPropertyEditor(); + } + } + override render() { const startNode: UmbTreeStartNode | undefined = this._startNodeId ? { unique: this._startNodeId, entityType: UMB_DOCUMENT_ENTITY_TYPE } @@ -64,7 +94,9 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl .startNode=${startNode} .value=${this.value} @change=${this.#onChange} - ?readonly=${this.readonly}> + ?readonly=${this.readonly} + .interactionMemories=${this._interactionMemories} + @interaction-memories-change=${this.#onInputInteractionMemoriesChange}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index dc2bfcbf1b48..177fd5e022ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -10,15 +10,18 @@ import { repeat, state, } from '@umbraco-cms/backoffice/external/lit'; +import { jsonStringComparison } from '@umbraco-cms/backoffice/observable-api'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UmbInteractionMemoriesChangeEvent } from '@umbraco-cms/backoffice/interaction-memory'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbSorterController, UmbSorterResolvePlacementAsGrid } from '@umbraco-cms/backoffice/sorter'; +import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/media-type'; import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; -import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; -import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/media-type'; import '@umbraco-cms/backoffice/imaging'; @@ -61,10 +64,10 @@ export class UmbInputMediaElement extends UmbFormControlMixin) { - this.#pickerContext.setSelection(ids); + this.#pickerInputContext.setSelection(ids); this.#sorter.setModel(ids); } public get selection(): Array { - return this.#pickerContext.getSelection(); + return this.#pickerInputContext.getSelection(); } @property({ type: Array }) @@ -146,13 +149,24 @@ export class UmbInputMediaElement extends UmbFormControlMixin | undefined { + return this.#pickerInputContext.interactionMemory.getAllMemories(); + } + public set interactionMemories(value: Array | undefined) { + this.#interactionMemories = value; + value?.forEach((memory) => this.#pickerInputContext.interactionMemory.setMemory(memory)); + } + + #interactionMemories?: Array = []; + @state() private _editMediaPath = ''; @state() private _cards: Array = []; - #pickerContext = new UmbMediaPickerInputContext(this); + #pickerInputContext = new UmbMediaPickerInputContext(this); constructor() { super(); @@ -166,15 +180,29 @@ export class UmbInputMediaElement extends UmbFormControlMixin (this.value = selection.join(','))); + this.observe(this.#pickerInputContext.selection, (selection) => (this.value = selection.join(','))); - this.observe(this.#pickerContext.selectedItems, async (selectedItems) => { + this.observe(this.#pickerInputContext.selectedItems, async (selectedItems) => { const missingCards = selectedItems.filter((item) => !this._cards.find((card) => card.unique === item.unique)); if (selectedItems?.length && !missingCards.length) return; this._cards = selectedItems ?? []; }); + this.observe( + this.#pickerInputContext.interactionMemory.memories, + (memories) => { + // only dispatch the event if the interaction memories have actually changed + const isIdentical = jsonStringComparison(memories, this.#interactionMemories); + + if (!isIdentical) { + this.#interactionMemories = memories; + this.dispatchEvent(new UmbInteractionMemoriesChangeEvent()); + } + }, + '_observeMemories', + ); + this.addValidator( 'rangeUnderflow', () => this.minMessage, @@ -188,7 +216,7 @@ export class UmbInputMediaElement extends UmbFormControlMixin 1, startNode: this.startNode, @@ -204,7 +232,7 @@ export class UmbInputMediaElement extends UmbFormControlMixin x.unique !== item.unique); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index 353400e6860c..d11cd168be37 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -18,6 +18,11 @@ import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/media-type'; import '@umbraco-cms/backoffice/imaging'; +import { + UmbInteractionMemoriesChangeEvent, + type UmbInteractionMemoryModel, +} from '@umbraco-cms/backoffice/interaction-memory'; +import { jsonStringComparison } from '@umbraco-cms/backoffice/observable-api'; type UmbRichMediaCardModel = { unique: string; @@ -102,7 +107,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< public override set value(value: Array | undefined) { super.value = value; this.#sorter.setModel(value); - this.#pickerContext.setSelection(value?.map((item) => item.mediaKey) ?? []); + this.#pickerInputContext.setSelection(value?.map((item) => item.mediaKey) ?? []); this.#itemManager.setUniques(value?.map((x) => x.mediaKey)); // Maybe the new value is using an existing media, and there we need to update the cards despite no repository update. this.#populateCards(); @@ -171,6 +176,17 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< } #readonly = false; + @property({ type: Array, attribute: false }) + public get interactionMemories(): Array | undefined { + return this.#pickerInputContext.interactionMemory.getAllMemories(); + } + public set interactionMemories(value: Array | undefined) { + this.#interactionMemories = value; + value?.forEach((memory) => this.#pickerInputContext.interactionMemory.setMemory(memory)); + } + + #interactionMemories?: Array = []; + @state() private _cards: Array = []; @@ -179,7 +195,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< readonly #itemManager = new UmbRepositoryItemsManager(this, UMB_MEDIA_ITEM_REPOSITORY_ALIAS); - readonly #pickerContext = new UmbMediaPickerInputContext(this); + readonly #pickerInputContext = new UmbMediaPickerInputContext(this); constructor() { super(); @@ -234,10 +250,24 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< this._routeBuilder = routeBuilder; }); - this.observe(this.#pickerContext.selection, (selection) => { + this.observe(this.#pickerInputContext.selection, (selection) => { this.#addItems(selection); }); + this.observe( + this.#pickerInputContext.interactionMemory.memories, + (memories) => { + // only dispatch the event if the interaction memories have actually changed + const isIdentical = jsonStringComparison(memories, this.#interactionMemories); + + if (!isIdentical) { + this.#interactionMemories = memories; + this.dispatchEvent(new UmbInteractionMemoriesChangeEvent()); + } + }, + '_observeMemories', + ); + this.addValidator( 'valueMissing', () => this.requiredMessage ?? UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, @@ -312,7 +342,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< } #openPicker() { - this.#pickerContext.openPicker( + this.#pickerInputContext.openPicker( { multiple: this.multiple, startNode: this.startNode, @@ -330,7 +360,7 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< async #onRemove(item: UmbRichMediaCardModel) { try { - await this.#pickerContext.requestRemoveItem(item.media); + await this.#pickerInputContext.requestRemoveItem(item.media); this.value = this.value?.filter((x) => x.key !== item.unique); this.dispatchEvent(new UmbChangeEvent()); } catch { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/constants.ts index 7ad378394b73..4274263cda7f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/constants.ts @@ -1,3 +1,4 @@ export { UMB_IMAGE_CROPPER_EDITOR_MODAL } from './image-cropper-editor/index.js'; -export * from './media-caption-alt-text/constants.js'; export { UMB_MEDIA_PICKER_MODAL } from './media-picker/index.js'; +export * from './media-caption-alt-text/constants.js'; +export * from './media-picker/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/constants.ts new file mode 100644 index 000000000000..252317b0597f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/constants.ts @@ -0,0 +1 @@ +export * from './media-picker.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 3ac92136f98b..a9a70f8f33e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,31 +1,34 @@ import { UmbMediaItemRepository } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; -import type { UmbMediaTreeItemModel, UmbMediaSearchItemModel, UmbMediaItemModel } from '../../types.js'; import { UmbMediaSearchProvider } from '../../search/index.js'; import type { UmbDropzoneMediaElement } from '../../dropzone/index.js'; +import type { UmbMediaTreeItemModel, UmbMediaSearchItemModel, UmbMediaItemModel } from '../../types.js'; +import { UmbMediaPickerContext } from './media-picker.context.js'; import type { UmbMediaPathModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; -import type { UmbDropzoneChangeEvent, UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone'; import { css, - html, customElement, - state, - repeat, + html, ifDefined, - query, - type PropertyValues, nothing, + query, + repeat, + state, } from '@umbraco-cms/backoffice/external/lit'; import { debounce, UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UMB_PROPERTY_TYPE_BASED_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; -import type { UUIInputEvent, UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; import { isUmbracoFolder } from '@umbraco-cms/backoffice/media-type'; -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UmbPickerModalBaseElement } from '@umbraco-cms/backoffice/picker'; +import { UMB_PROPERTY_TYPE_BASED_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/content'; import { UMB_VARIANT_CONTEXT } from '@umbraco-cms/backoffice/variant'; +import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbDropzoneChangeEvent, UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; +import type { UmbPickerContext } from '@umbraco-cms/backoffice/picker'; +import type { UUIInputEvent, UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui'; import '@umbraco-cms/backoffice/imaging'; @@ -33,11 +36,19 @@ const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_M // TODO: investigate how we can reuse the picker-search-field element, picker context etc. @customElement('umb-media-picker-modal') -export class UmbMediaPickerModalElement extends UmbModalBaseElement { +export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement< + UmbMediaItemModel, + UmbMediaPickerModalData, + UmbMediaPickerModalValue +> { #mediaTreeRepository = new UmbMediaTreeRepository(this); #mediaItemRepository = new UmbMediaItemRepository(this); #mediaSearchProvider = new UmbMediaSearchProvider(this); + /* TODO: We currently only rely on the interactionMemory manager in the picker interface which is correctly implemented in the Media Picker + Remove this type cast when MediaPicker has implemented the full PickerContext interface */ + protected override _pickerContext = new UmbMediaPickerContext(this) as unknown as UmbPickerContext; + #dataType?: { unique: string }; @state() @@ -78,6 +89,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement(); #contextCulture?: string | null; + #locationInteractionMemoryUnique: string = 'UmbMediaItemPickerLocation'; constructor() { super(); @@ -106,25 +118,36 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement x !== null && x !== undefined, + ); + + if (uniquesToRequest.length > 0) { + const { data } = await this.#mediaItemRepository.requestItems(uniquesToRequest); + + this._startNode = data?.find((x) => x.unique === startNode?.unique); + const locationMemoryItem = data?.find((x) => x.unique === locationFromMemory?.entity.unique); - if (startNode) { - const { data } = await this.#mediaItemRepository.requestItems([startNode.unique]); - this._startNode = data?.[0]; + // TODO: We probably need to check if the location item is within the start node. If not then fall back to start node. + const source = locationMemoryItem || this._startNode; - if (this._startNode) { + if (source) { this._currentMediaEntity = { - name: this._startNode.name, - unique: this._startNode.unique, - entityType: this._startNode.entityType, + name: source.name, + unique: source.unique, + entityType: source.entityType, }; - this._searchFrom = { unique: this._startNode.unique, entityType: this._startNode.entityType }; + this._searchFrom = { unique: source.unique, entityType: source.entityType }; } } this.#loadChildrenOfCurrentMediaItem(); } + // TODO: move to location manager in context async #loadChildrenOfCurrentMediaItem(selectedItems?: Array) { const key = this._currentMediaEntity.entityType + this._currentMediaEntity.unique; let paginationManager = this.#pagingMap.get(key); @@ -166,13 +189,14 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement { this.#searchMedia(); }, 500); + // TODO: move to search manager in context #onSearch(e: UUIInputEvent) { this._searchQuery = (e.target.value as string).toLocaleLowerCase(); this._searching = true; @@ -260,6 +290,8 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker.context.token.ts new file mode 100644 index 000000000000..f1bf040da979 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker.context.token.ts @@ -0,0 +1,8 @@ +import type { UmbMediaPickerContext } from './media-picker.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_MEDIA_PICKER_CONTEXT = new UmbContextToken( + 'UmbPickerContext', + undefined, + (context): context is UmbMediaPickerContext => context.IS_MEDIA_PICKER_CONTEXT, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker.context.ts new file mode 100644 index 000000000000..88ef119fdcc4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker.context.ts @@ -0,0 +1,18 @@ +import { UMB_MEDIA_PICKER_CONTEXT } from './media-picker.context.token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbInteractionMemoryManager } from '@umbraco-cms/backoffice/interaction-memory'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +// TODO: extend UmbTreeItemPickerContext +export class UmbMediaPickerContext extends UmbContextBase { + // For context token safety: + public readonly IS_MEDIA_PICKER_CONTEXT = true; + + public readonly interactionMemory = new UmbInteractionMemoryManager(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_MEDIA_PICKER_CONTEXT); + } +} + +export { UmbMediaPickerContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts index d2e5d0a576bf..29e8c9c72d46 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts @@ -2,30 +2,31 @@ import type { UmbInputRichMediaElement } from '../../components/input-rich-media import type { UmbCropModel, UmbMediaPickerValueModel } from '../types.js'; import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyEditorUiInteractionMemoryManager } from '@umbraco-cms/backoffice/property-editor'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; -import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; +import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; -import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import '../../components/input-rich-media/input-rich-media.element.js'; -import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; - -const elementName = 'umb-property-editor-ui-media-picker'; - /** * @element umb-property-editor-ui-media-picker */ -@customElement(elementName) +@customElement('umb-property-editor-ui-media-picker') export class UmbPropertyEditorUIMediaPickerElement extends UmbFormControlMixin(UmbLitElement) implements UmbPropertyEditorUiElement { public set config(config: UmbPropertyEditorConfigCollection | undefined) { + this.#interactionMemoryManager.setPropertyEditorConfig(config); + if (!config) return; this._allowedMediaTypes = config.getValueByAlias('filter')?.split(',') ?? []; @@ -87,6 +88,13 @@ export class UmbPropertyEditorUIMediaPickerElement @state() private _variantId?: string; + @state() + private _interactionMemories: Array = []; + + #interactionMemoryManager = new UmbPropertyEditorUiInteractionMemoryManager(this, { + memoryUniquePrefix: 'UmbMediaPicker', + }); + constructor() { super(); @@ -94,6 +102,10 @@ export class UmbPropertyEditorUIMediaPickerElement this.observe(context?.alias, (alias) => (this._alias = alias)); this.observe(context?.variantId, (variantId) => (this._variantId = variantId?.toString() || 'invariant')); }); + + this.observe(this.#interactionMemoryManager.memoriesForPropertyEditor, (interactionMemories) => { + this._interactionMemories = interactionMemories ?? []; + }); } override firstUpdated() { @@ -110,6 +122,17 @@ export class UmbPropertyEditorUIMediaPickerElement this.dispatchEvent(new UmbChangeEvent()); } + async #onInputInteractionMemoriesChange(event: UmbChangeEvent) { + const target = event.target as UmbInputRichMediaElement; + const interactionMemories = target.interactionMemories; + + if (interactionMemories && interactionMemories.length > 0) { + await this.#interactionMemoryManager.saveMemoriesForPropertyEditor(interactionMemories); + } else { + await this.#interactionMemoryManager.deleteMemoriesForPropertyEditor(); + } + } + override render() { return html` + ?readonly=${this.readonly} + .interactionMemories=${this._interactionMemories} + @interaction-memories-change=${this.#onInputInteractionMemoriesChange}> `; } @@ -136,6 +161,6 @@ export { UmbPropertyEditorUIMediaPickerElement as element }; declare global { interface HTMLElementTagNameMap { - [elementName]: UmbPropertyEditorUIMediaPickerElement; + ['umb-property-editor-ui-media-picker']: UmbPropertyEditorUIMediaPickerElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts index 91e8f177d5d6..a557488c1e4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts @@ -1,9 +1,11 @@ import type { UmbContentPickerSource } from '../../types.js'; -import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UmbInteractionMemoriesChangeEvent } from '@umbraco-cms/backoffice/interaction-memory'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; import type { UmbReferenceByUniqueAndType } from '@umbraco-cms/backoffice/models'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; @@ -72,7 +74,17 @@ export class UmbInputContentElement extends UmbFormControlMixin | undefined { + return this.#interactionMemories; + } + public set interactionMemories(value: Array | undefined) { + this.#interactionMemories = value; + } + + #interactionMemories: Array | undefined; #entityTypeLookup = { content: 'document', media: 'media', member: 'member' }; @@ -88,6 +100,15 @@ export class UmbInputContentElement extends UmbFormControlMixin + @change=${this.#onChange} + .interactionMemories=${this.#interactionMemories} + @interaction-memories-change=${this.#onInteractionMemoriesChange}> `; } @@ -126,7 +149,9 @@ export class UmbInputContentElement extends UmbFormControlMixin + @change=${this.#onChange} + .interactionMemories=${this.#interactionMemories} + @interaction-memories-change=${this.#onInteractionMemoriesChange}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index f34caa2ca14c..a80582396006 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -10,6 +10,8 @@ import { UMB_ANCESTORS_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { UMB_DOCUMENT_ENTITY_TYPE } from '@umbraco-cms/backoffice/document'; import { UMB_MEDIA_ENTITY_TYPE } from '@umbraco-cms/backoffice/media'; import { UMB_MEMBER_ENTITY_TYPE } from '@umbraco-cms/backoffice/member'; +import { UmbPropertyEditorUiInteractionMemoryManager } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbInteractionMemoryModel } from '@umbraco-cms/backoffice/interaction-memory'; import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, @@ -65,6 +67,9 @@ export class UmbPropertyEditorUIContentPickerElement @state() private _invalidData?: UmbContentPickerValueType; + @state() + private _interactionMemories: Array = []; + #dynamicRoot?: UmbContentPickerSource['dynamicRoot']; #dynamicRootRepository = new UmbContentPickerDynamicRootRepository(this); @@ -74,7 +79,21 @@ export class UmbPropertyEditorUIContentPickerElement member: UMB_MEMBER_ENTITY_TYPE, }; + #interactionMemoryManager = new UmbPropertyEditorUiInteractionMemoryManager(this, { + memoryUniquePrefix: 'UmbContentPicker', + }); + + constructor() { + super(); + + this.observe(this.#interactionMemoryManager.memoriesForPropertyEditor, (interactionMemories) => { + this._interactionMemories = interactionMemories ?? []; + }); + } + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + this.#interactionMemoryManager.setPropertyEditorConfig(config); + if (!config) return; const startNode = config.getValueByAlias('startNode'); @@ -160,6 +179,17 @@ export class UmbPropertyEditorUIContentPickerElement this.readonly = false; } + async #onInputInteractionMemoriesChange(event: UmbChangeEvent) { + const target = event.target as UmbInputContentElement; + const interactionMemories = target.interactionMemories; + + if (interactionMemories && interactionMemories.length > 0) { + await this.#interactionMemoryManager.saveMemoriesForPropertyEditor(interactionMemories); + } else { + await this.#interactionMemoryManager.deleteMemoriesForPropertyEditor(); + } + } + override render() { const startNode: UmbTreeStartNode | undefined = this._rootUnique && this._rootEntityType @@ -177,7 +207,9 @@ export class UmbPropertyEditorUIContentPickerElement .startNode=${startNode} .allowedContentTypeIds=${this._allowedContentTypeUniques ?? ''} ?readonly=${this.readonly} - @change=${this.#onChange}> + @change=${this.#onChange} + .interactionMemories=${this._interactionMemories} + @interaction-memories-change=${this.#onInputInteractionMemoriesChange}> ${this.#renderInvalidData()} `; diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index c2ec31a43d91..f99389051c88 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -86,6 +86,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/icon": ["./src/packages/core/icon-registry/index.ts"], "@umbraco-cms/backoffice/id": ["./src/packages/core/id/index.ts"], "@umbraco-cms/backoffice/imaging": ["./src/packages/media/imaging/index.ts"], + "@umbraco-cms/backoffice/interaction-memory": ["./src/packages/core/interaction-memory/index.ts"], "@umbraco-cms/backoffice/language": ["./src/packages/language/index.ts"], "@umbraco-cms/backoffice/lit-element": ["./src/packages/core/lit-element/index.ts"], "@umbraco-cms/backoffice/localization": ["./src/packages/core/localization/index.ts"],