Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
6b450ed
set property type unique on context
madsrasmussen Sep 8, 2025
7affbcf
set the value
madsrasmussen Sep 8, 2025
46e7667
observe property type unique from content picker property editor
madsrasmussen Sep 8, 2025
f275173
remove unused
madsrasmussen Sep 8, 2025
7315714
observe data type unique
madsrasmussen Sep 8, 2025
2d62fcb
wip picker memories
madsrasmussen Sep 9, 2025
e92ef18
append memory option to the picker data model
madsrasmussen Sep 9, 2025
a535abb
split into methods
madsrasmussen Sep 9, 2025
ca931a1
initialize memory context
madsrasmussen Sep 9, 2025
eb48cf2
rename arg
madsrasmussen Sep 9, 2025
3b01d18
make memory module
madsrasmussen Sep 9, 2025
b4bf867
export constants
madsrasmussen Sep 9, 2025
77d77bd
allow nested memories
madsrasmussen Sep 9, 2025
f72846d
pass memory from input document to picker context
madsrasmussen Sep 10, 2025
2e4682e
Update property-editor-ui-content-picker.element.ts
madsrasmussen Sep 10, 2025
369d0fb
fix import
madsrasmussen Sep 10, 2025
edb457f
prefix with interaction
madsrasmussen Sep 10, 2025
7634e58
clean up
madsrasmussen Sep 10, 2025
9dd0225
fix import
madsrasmussen Sep 10, 2025
cbb4097
rename module
madsrasmussen Sep 10, 2025
4ec1a98
Update vite.config.ts
madsrasmussen Sep 10, 2025
6727171
update module name
madsrasmussen Sep 10, 2025
141b067
observe after search is initialized
madsrasmussen Sep 10, 2025
8fde539
use memory manager in all places
madsrasmussen Sep 10, 2025
49ec314
make picker modal base element
madsrasmussen Sep 10, 2025
a8ea47b
update types
madsrasmussen Sep 10, 2025
b36dddd
add memory for document picker property editor
madsrasmussen Sep 10, 2025
f7aec47
store tree item picker expansion state in interaction memory
madsrasmussen Sep 15, 2025
e98369a
Update picker-modal-base.element.ts
madsrasmussen Sep 15, 2025
533cbfa
remove the memory if we have no expansion state
madsrasmussen Sep 15, 2025
482b81f
delete memory if it doesn't include anything
madsrasmussen Sep 15, 2025
1845e39
clear picker input memories if nothing comes from the modal
madsrasmussen Sep 15, 2025
5976f0b
Refactor interaction memory handling in picker input
madsrasmussen Sep 15, 2025
69d420c
only dispatch an event if the value changes
madsrasmussen Sep 15, 2025
5fbeea8
remove unused
madsrasmussen Sep 15, 2025
c5a10e5
observe to support close on escape
madsrasmussen Sep 15, 2025
bfd122b
add comments
madsrasmussen Sep 15, 2025
82e584d
fix type error
madsrasmussen Sep 15, 2025
71dc79f
fix typings
madsrasmussen Sep 15, 2025
072df2c
Replaces data type-based memory keys with config hash-based keys
madsrasmussen Sep 15, 2025
1f1a56e
dont store picker search in interaction memory
madsrasmussen Sep 15, 2025
6578e6c
Rename interaction memory key in picker modal base
madsrasmussen Sep 15, 2025
ef70022
Remove error throw for missing interaction memory
madsrasmussen Sep 15, 2025
c7f7aaa
Refactor interaction memory handling in content picker
madsrasmussen Sep 15, 2025
aaa7503
Refactor content picker to use interaction memories
madsrasmussen Sep 15, 2025
f36c5b8
remove debugger
madsrasmussen Sep 15, 2025
f0875a1
rename const
madsrasmussen Sep 15, 2025
afadcfb
wip media picker memories
madsrasmussen Sep 15, 2025
fa5883b
remove args
madsrasmussen Sep 15, 2025
51d30a2
simplify memory model
madsrasmussen Sep 15, 2025
340b888
Merge branch 'main' into v16/feature/picker-expansion-retention
madsrasmussen Sep 15, 2025
c1afe9d
update internal value before dispatching event
madsrasmussen Sep 15, 2025
fa9fc6c
remove unused
madsrasmussen Sep 15, 2025
b0985b0
Update property-type-based-property.element.ts
madsrasmussen Sep 15, 2025
44bbd23
rename method
madsrasmussen Sep 16, 2025
3c0e8f1
simplify types
madsrasmussen Sep 16, 2025
eea6962
implement location memory for media picker
madsrasmussen Sep 16, 2025
13816cf
temp type cast
madsrasmussen Sep 16, 2025
25383d8
set location memory when using the breadcrumb
madsrasmussen Sep 16, 2025
d900b7c
remove code duplication
madsrasmussen Sep 16, 2025
4c64290
bubble memories from input media to input content
madsrasmussen Sep 16, 2025
93787c3
Update src/Umbraco.Web.UI.Client/src/packages/property-editors/conten…
madsrasmussen Sep 16, 2025
4cb4133
fix import
madsrasmussen Sep 16, 2025
79b4e03
Merge branch 'v16/feature/picker-expansion-retention' of https://gith…
madsrasmussen Sep 16, 2025
bfaf0ec
remove unused method
madsrasmussen Sep 16, 2025
68c4b13
Refactor content picker interaction memory management
madsrasmussen Sep 16, 2025
b963a3a
Refactor interaction memory management in pickers
madsrasmussen Sep 16, 2025
933ad19
export context token
madsrasmussen Sep 16, 2025
46380d4
add js docs
madsrasmussen Sep 16, 2025
5e5d15b
remove timestamp
madsrasmussen Sep 16, 2025
71cf680
add tests for interaction memory manager
madsrasmussen Sep 16, 2025
b9df295
Added tests for the property editor ui interaction memory manager
madsrasmussen Sep 16, 2025
3d801ed
Rename memories to memoriesForPropertyEditor
madsrasmussen Sep 16, 2025
94865c7
Separated out `import type`s + ordering
leekelleher Sep 17, 2025
bf59f3d
remove interaction memory implementation in modal context
madsrasmussen Sep 18, 2025
6dc88f8
remove interactionMemories from modal interface
madsrasmussen Sep 18, 2025
245e6eb
revert to using the umbOpenModal helper
madsrasmussen Sep 18, 2025
7030ba0
align property and event name
madsrasmussen Sep 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 7 additions & 4 deletions src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './interaction-memory.context.token.js';
Original file line number Diff line number Diff line change
@@ -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 });
}
}
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -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>(
'UmbInteractionMemoryContext',
);
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -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<UmbInteractionMemoryModel>([], (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<UmbInteractionMemoryModel | undefined>)} An observable that emits the memory item or undefined if not found.
* @memberof UmbInteractionMemoryManager
*/
memory(unique: string): Observable<UmbInteractionMemoryModel | undefined> {
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<UmbInteractionMemoryModel>} An array of all memory items.
* @memberof UmbInteractionMemoryManager
*/
getAllMemories(): Array<UmbInteractionMemoryModel> {
return this.#memories.getValue();
}

/**
* Clear all memory items from the manager.
* @memberof UmbInteractionMemoryManager
*/
clear() {
this.#memories.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface UmbInteractionMemoryModel {
unique: string;
value?: any;
memories?: Array<UmbInteractionMemoryModel>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
Original file line number Diff line number Diff line change
@@ -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<
Expand All @@ -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.
Expand Down Expand Up @@ -100,6 +107,7 @@ export class UmbPickerInputContext<
selection: this.getSelection(),
} as PickerModalValueType,
}).catch(() => undefined);

if (!modalValue) return;

this.setSelection(modalValue.selection);
Expand Down
3 changes: 2 additions & 1 deletion src/Umbraco.Web.UI.Client/src/packages/core/picker/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './picker-modal-base.element.js';
Original file line number Diff line number Diff line change
@@ -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<ItemType> = UmbPickerModalData<ItemType>,
ModalValueType = unknown,
ModalManifestType extends ManifestModal = ManifestModal,
> extends UmbModalBaseElement<ModalDataType, ModalValueType, ModalManifestType> {
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<UmbInteractionMemoryModel>) {
if (pickerMemories?.length > 0) {
const pickerModalMemory: UmbInteractionMemoryModel = {
unique: this.#getInteractionMemoryUnique(),
memories: pickerMemories,
};

this.#pickerInputContext?.interactionMemory.setMemory(pickerModalMemory);
} else {
this.#pickerInputContext?.interactionMemory.deleteMemory(this.#getInteractionMemoryUnique());
}
}
}
Loading
Loading