diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 1d98a6c6bf..3d5beadca1 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -28,6 +28,7 @@ "./content-type": "./dist-cms/packages/core/content-type/index.js", "./content": "./dist-cms/packages/core/content/index.js", "./culture": "./dist-cms/packages/core/culture/index.js", + "./picker": "./dist-cms/packages/core/picker/index.js", "./current-user": "./dist-cms/packages/user/current-user/index.js", "./data-type": "./dist-cms/packages/data-type/index.js", "./debug": "./dist-cms/packages/core/debug/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts index 10dd58fa03..24ccefc904 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.context.ts @@ -129,6 +129,12 @@ export class UmbBlockGridAreaTypeWorkspaceContext throw new Error('You cannot set a name of a area-type.'); } + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: keyof UmbBlockGridTypeAreaType) { return this.#data.asObservablePart((data) => data?.[propertyAlias as keyof UmbBlockGridTypeAreaType] as ReturnType); } @@ -137,6 +143,13 @@ export class UmbBlockGridAreaTypeWorkspaceContext return this.#data.getValue()?.[propertyAlias as keyof UmbBlockGridTypeAreaType] as ReturnType; } + /** + * @function setPropertyValue + * @param {string} alias + * @param {unknown} value - value can be a promise resolving into the actual value or the raw value it self. + * @returns {Promise} + * @description Set the value of this property. + */ async setPropertyValue(alias: string, value: unknown) { const currentData = this.#data.value; if (currentData) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-inline-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-inline-property-dataset.context.ts index 6f8f9c6a95..d57f88b590 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-inline-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-inline-property-dataset.context.ts @@ -36,8 +36,10 @@ export class UmbBlockGridInlinePropertyDatasetContext extends UmbControllerBase } /** - * TODO: Write proper JSDocs here. - * @param propertyAlias + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. */ async propertyValueByAlias(propertyAlias: string) { // TODO: Investigate how I do that with the workspaces.. @@ -45,9 +47,11 @@ export class UmbBlockGridInlinePropertyDatasetContext extends UmbControllerBase } /** - * TODO: Write proper JSDocs here. - * @param propertyAlias - * @param value + * @function setPropertyValue + * @param {string} propertyAlias + * @param {unknown} value - value can be a promise resolving into the actual value or the raw value it self. + * @returns {Promise} + * @description Set the value of this property. */ async setPropertyValue(propertyAlias: string, value: unknown) { // TODO: Investigate how I do that with the workspaces.. diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts index 74b2b8bfcb..c50aad7dbb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.context.ts @@ -136,6 +136,12 @@ export class UmbBlockTypeWorkspaceContext | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string) { return this.#data.asObservablePart((data) => data?.[propertyAlias as keyof BlockTypeData] as ReturnType); } @@ -144,6 +150,13 @@ export class UmbBlockTypeWorkspaceContext} + * @description Set the value of this property. + */ async setPropertyValue(alias: string, value: unknown) { const currentData = this.#data.value; if (currentData) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts index be65432e2b..cb1f037a1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts @@ -2,10 +2,11 @@ import type { UmbBlockDataType } from '../types.js'; import { UmbBlockElementPropertyDatasetContext } from './block-element-property-dataset.context.js'; import type { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type'; import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbClassState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { type UmbClassInterface, UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbDocumentTypeDetailRepository } from '@umbraco-cms/backoffice/document-type'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; export class UmbBlockElementManager extends UmbControllerBase { @@ -17,6 +18,9 @@ export class UmbBlockElementManager extends UmbControllerBase { }); #getDataResolver!: () => void; + #variantId = new UmbClassState(undefined); + readonly variantId = this.#variantId.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.udi); readonly contentTypeId = this.#data.asObservablePart((data) => data?.contentTypeKey); @@ -42,6 +46,10 @@ export class UmbBlockElementManager extends UmbControllerBase { this.#data.setValue(undefined); } + setVariantId(variantId: UmbVariantId | undefined) { + this.#variantId.setValue(variantId); + } + setData(data: UmbBlockDataType | undefined) { this.#data.setValue(data); this.#getDataResolver(); @@ -63,6 +71,18 @@ export class UmbBlockElementManager extends UmbControllerBase { return this.getData()?.contentTypeKey; } + // We will implement propertyAlias in the future, when implementing Varying Blocks. [NL] + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async propertyVariantId(propertyAlias: string) { + return this.variantId; + } + + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string) { await this.#getDataPromise; @@ -75,6 +95,13 @@ export class UmbBlockElementManager extends UmbControllerBase { return this.#data.getValue()?.[propertyAlias] as ReturnType; } + /** + * @function setPropertyValue + * @param {string} alias + * @param {unknown} value - value can be a promise resolving into the actual value or the raw value it self. + * @returns {Promise} + * @description Set the value of this property. + */ async setPropertyValue(alias: string, value: unknown) { this.initiatePropertyValueChange(); await this.#getDataPromise; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context.ts index 22a0d90c66..8c6064d601 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-property-dataset.context.ts @@ -35,20 +35,28 @@ export class UmbBlockElementPropertyDatasetContext extends UmbControllerBase imp this.provideContext(UMB_BLOCK_ELEMENT_PROPERTY_DATASET_CONTEXT, this); } + propertyVariantId?(propertyAlias: string): Promise> { + return this.#elementManager.propertyVariantId(propertyAlias); + } + /** - * TODO: Write proper JSDocs here. - * @param propertyAlias + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. */ async propertyValueByAlias(propertyAlias: string) { return await this.#elementManager.propertyValueByAlias(propertyAlias); } /** - * TODO: Write proper JSDocs here. - * @param propertyAlias - * @param value + * @function setPropertyValue + * @param {string} alias + * @param {unknown} value - value can be a promise resolving into the actual value or the raw value it self. + * @returns {Promise} + * @description Set the value of this property. */ - async setPropertyValue(propertyAlias: string, value: unknown) { - return this.#elementManager.setPropertyValue(propertyAlias, value); + async setPropertyValue(alias: string, value: unknown) { + return this.#elementManager.setPropertyValue(alias, value); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index 0f1fb6eff3..5adfeb96f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -6,7 +6,7 @@ import { type UmbRoutableWorkspaceContext, UmbWorkspaceIsNewRedirectController, } from '@umbraco-cms/backoffice/workspace'; -import { UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbClassState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal'; @@ -16,6 +16,8 @@ import { UMB_BLOCK_MANAGER_CONTEXT, type UmbBlockWorkspaceData, } from '@umbraco-cms/backoffice/block'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; export type UmbBlockWorkspaceElementManagerNames = 'content' | 'settings'; export class UmbBlockWorkspaceContext @@ -55,6 +57,9 @@ export class UmbBlockWorkspaceContext(undefined); readonly name = this.#label.asObservable(); + #variantId = new UmbClassState(undefined); + readonly variantId = this.#variantId.asObservable(); + constructor(host: UmbControllerHost, workspaceArgs: { manifest: ManifestWorkspace }) { super(host, workspaceArgs.manifest.alias); const manifest = workspaceArgs.manifest; @@ -86,6 +91,16 @@ export class UmbBlockWorkspaceContext { + this.observe(context.variantId, (variantId) => { + this.#variantId.setValue(variantId); + }); + }); + + this.observe(this.variantId, (variantId) => { + this.content.setVariantId(variantId); + }); + this.routes.setRoutes([ { path: 'create/:elementTypeKey', @@ -297,8 +312,12 @@ export class UmbBlockWorkspaceContext | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: propertyAliasType) { return this.#layout.asObservablePart( (layout) => layout?.[propertyAlias as keyof LayoutDataType] as LayoutDataType[propertyAliasType], @@ -310,6 +329,13 @@ export class UmbBlockWorkspaceContext} + * @description Set the value of this property. + */ async setPropertyValue(alias: string, value: unknown) { const currentData = this.#layout.value; if (currentData) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/collection-item-picker-modal.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/collection-item-picker-modal.context.ts new file mode 100644 index 0000000000..b620f2d892 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/collection-item-picker-modal.context.ts @@ -0,0 +1,10 @@ +import { UmbPickerContext } from '@umbraco-cms/backoffice/picker'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbCollectionItemPickerContext extends UmbPickerContext { + constructor(host: UmbControllerHost) { + super(host); + } +} + +export { UmbCollectionItemPickerContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/index.ts new file mode 100644 index 0000000000..e5f1b5e677 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/index.ts @@ -0,0 +1 @@ +export * from './collection-item-picker-modal.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts index 3e54ec50e1..e3b5fac4d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/index.ts @@ -5,6 +5,7 @@ import './components/index.js'; export * from './default/collection-default.element.js'; export * from './collection.element.js'; export * from './components/index.js'; +export * from './collection-item-picker-modal/index.js'; export * from './default/collection-default.context.js'; export * from './default/collection-default.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts index 7047c7c239..bc4ad0c969 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/ref-item/ref-item.element.ts @@ -31,7 +31,7 @@ export class UmbRefItemElement extends UmbElementMixin(UUIRefElement) { @click=${this.handleOpenClick} @keydown=${this.handleOpenKeydown} ?disabled=${this.disabled}> - ${when(this.icon, () => html``)} + ${when(this.icon, () => html``)}
${this.name}
${this.detail} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/content-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/content-property-dataset.context.ts index 1f981f0da4..8975b77b29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/content-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/content-property-dataset.context.ts @@ -113,16 +113,12 @@ export class UmbContentPropertyDatasetContext< /** * @function setPropertyValueByVariant * @param {string} propertyAlias - * @param {PromiseLike} value - value can be a promise resolving into the actual value or the raw value it self. + * @param {unknown} value - value can be a promise resolving into the actual value or the raw value it self. * @param {UmbVariantId} propertyVariantId - The variant id for the value to be set for. * @returns {Promise} * @description Get the value of this property. */ - setPropertyValueByVariant( - propertyAlias: string, - value: PromiseLike, - propertyVariantId: UmbVariantId, - ): Promise { + setPropertyValueByVariant(propertyAlias: string, value: unknown, propertyVariantId: UmbVariantId): Promise { return this.#workspace.setPropertyValue(propertyAlias, value, propertyVariantId); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts index 47e891bd60..3adb526942 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/index.ts @@ -72,6 +72,7 @@ import type { ManifestEntryPoint } from './entry-point.model.js'; import type { ManifestMonacoMarkdownEditorAction } from './monaco-markdown-editor-action.model.js'; import type { ManifestSectionRoute } from './section-route.model.js'; import type { ManifestBase, ManifestBundle, ManifestCondition } from '@umbraco-cms/backoffice/extension-api'; +import type { ManifestPickerSearchResultItem } from './picker-search-result-item.model.js'; export type * from './app-entry-point.model.js'; export type * from './auth-provider.model.js'; @@ -100,6 +101,7 @@ export type * from './mfa-login-provider.model.js'; export type * from './modal.model.js'; export type * from './monaco-markdown-editor-action.model.js'; export type * from './package-view.model.js'; +export type * from './picker-search-result-item.model.js'; export type * from './preview-app.model.js'; export type * from './property-action.model.js'; export type * from './property-editor.model.js'; @@ -188,6 +190,7 @@ export type ManifestTypes = | ManifestModal | ManifestMonacoMarkdownEditorAction | ManifestPackageView + | ManifestPickerSearchResultItem | ManifestPreviewAppProvider | ManifestPropertyActions | ManifestPropertyEditorSchema @@ -196,10 +199,10 @@ export type ManifestTypes = | ManifestSearchProvider | ManifestSearchResultItem | ManifestSection + | ManifestSectionRoute | ManifestSectionSidebarApp | ManifestSectionSidebarAppMenuKind | ManifestSectionView - | ManifestSectionRoute | ManifestStore | ManifestTheme | ManifestTinyMcePlugin diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/picker-search-result-item.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/picker-search-result-item.model.ts new file mode 100644 index 0000000000..8797323f40 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/picker-search-result-item.model.ts @@ -0,0 +1,9 @@ +import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; + +/** + * Represents a picker search result element. + */ +export interface ManifestPickerSearchResultItem extends ManifestElementAndApi { + type: 'pickerSearchResultItem'; + forEntityTypes: Array; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts index a9e6fb1ecd..0f01fd1342 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts @@ -10,6 +10,7 @@ import { manifests as extensionManifests } from './extension-registry/manifests. import { manifests as iconRegistryManifests } from './icon-registry/manifests.js'; import { manifests as localizationManifests } from './localization/manifests.js'; import { manifests as modalManifests } from './modal/common/manifests.js'; +import { manifests as pickerManifests } from './picker/manifests.js'; import { manifests as propertyActionManifests } from './property-action/manifests.js'; import { manifests as propertyTypeManifests } from './property-type/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; @@ -24,24 +25,25 @@ import type { ManifestTypes, UmbBackofficeManifestKind } from './extension-regis export const manifests: Array = [ ...authManifests, - ...extensionManifests, - ...iconRegistryManifests, - ...cultureManifests, - ...localizationManifests, - ...themeManifests, - ...sectionManifests, - ...treeManifests, ...collectionManifests, - ...workspaceManifests, ...contentManifests, ...contentTypeManifests, - ...propertyTypeManifests, - ...settingsManifests, - ...modalManifests, + ...cultureManifests, + ...debugManifests, ...entityActionManifests, ...entityBulkActionManifests, + ...extensionManifests, + ...iconRegistryManifests, + ...localizationManifests, + ...modalManifests, + ...pickerManifests, ...propertyActionManifests, - ...serverFileSystemManifests, - ...debugManifests, + ...propertyTypeManifests, ...recycleBinManifests, + ...sectionManifests, + ...serverFileSystemManifests, + ...settingsManifests, + ...themeManifests, + ...treeManifests, + ...workspaceManifests, ]; 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 992fb667a5..f2d55fad4f 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 @@ -2,7 +2,14 @@ export interface UmbPickerModalData { multiple?: boolean; filter?: (item: ItemType) => boolean; pickableFilter?: (item: ItemType) => boolean; + search?: UmbPickerModalSearchConfig; } + +export interface UmbPickerModalSearchConfig { + providerAlias: string; + queryParams?: object; +} + export interface UmbPickerModalValue { selection: Array; } 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 new file mode 100644 index 0000000000..b19f3e47db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/index.ts @@ -0,0 +1,4 @@ +export * from './search/index.js'; +export * from './picker.context.js'; +export * from './picker.context.token.js'; +export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/manifests.ts new file mode 100644 index 0000000000..7504bc1bc5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as searchManifests } from './search/manifests.js'; +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...searchManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.token.ts new file mode 100644 index 0000000000..3973df1bfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.token.ts @@ -0,0 +1,4 @@ +import type { UmbPickerContext } from './picker.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_PICKER_CONTEXT = new UmbContextToken('UmbPickerContext'); 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 new file mode 100644 index 0000000000..b586a90e3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/picker.context.ts @@ -0,0 +1,14 @@ +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 type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; + +export class UmbPickerContext extends UmbContextBase { + public readonly selection = new UmbSelectionManager(this); + public readonly search = new UmbPickerSearchManager(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_PICKER_CONTEXT); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/index.ts new file mode 100644 index 0000000000..c943b37222 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/index.ts @@ -0,0 +1,3 @@ +export * from './manager/index.js'; +export * from './picker-search-result.element.js'; +export * from './picker-search-field.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/index.ts new file mode 100644 index 0000000000..2eea098988 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/index.ts @@ -0,0 +1,2 @@ +export * from './picker-search.manager.js'; +export * from './types.js'; 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 new file mode 100644 index 0000000000..3b09cd0927 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/picker-search.manager.ts @@ -0,0 +1,193 @@ +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 { UmbArrayState, UmbBooleanState, UmbNumberState, UmbObjectState } from '@umbraco-cms/backoffice/observable-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. + * @exports + * @class UmbPickerSearchManager + * @augments {UmbControllerBase} + * @template ResultItemType + * @template QueryType + */ +export class UmbPickerSearchManager< + ResultItemType extends UmbSearchResultItemModel = UmbSearchResultItemModel, + QueryType extends UmbSearchRequestArgs = UmbSearchRequestArgs, +> extends UmbControllerBase { + #searchable = new UmbBooleanState(false); + public readonly searchable = this.#searchable.asObservable(); + + #query = new UmbObjectState(undefined); + public readonly query = this.#query.asObservable(); + + #searching = new UmbBooleanState(false); + public readonly searching = this.#searching.asObservable(); + + #resultItems = new UmbArrayState([], (x) => x.unique); + public readonly resultItems = this.#resultItems.asObservable(); + + #resultTotalItems = new UmbNumberState(0); + public readonly resultTotalItems = this.#resultTotalItems.asObservable(); + + #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. + * @memberof UmbPickerSearchManager + */ + public setConfig(config: UmbPickerSearchManagerConfig) { + this.#config = config; + this.#initSearch(); + } + + /** + * Get the current configuration for the search manager. + * @returns {UmbPickerSearchManagerConfig | undefined} The current configuration for the search manager. + * @memberof UmbPickerSearchManager + */ + public getConfig(): UmbPickerSearchManagerConfig | undefined { + return this.#config; + } + + /** + * Update the current configuration for the search manager. + * @param {Partial} partialConfig + * @memberof UmbPickerSearchManager + */ + public updateConfig(partialConfig: Partial) { + const mergedConfig = { ...this.#config, ...partialConfig } as UmbPickerSearchManagerConfig; + this.setConfig(mergedConfig); + } + + /** + * Returns whether items can be searched. + * @returns {boolean} Whether items can be searched. + * @memberof UmbPickerSearchManager + */ + public getSearchable(): boolean { + return this.#searchable.getValue(); + } + + /** + * Sets whether items can be searched. + * @param {boolean} value Whether items can be searched. + * @memberof UmbPickerSearchManager + */ + public setSearchable(value: boolean) { + this.#searchable.setValue(value); + } + + /** + * Search for items based on the current query. + * @memberof UmbPickerSearchManager + */ + public search() { + if (this.getSearchable() === false) throw new Error('Search is not enabled'); + + const query = this.#query.getValue(); + if (!query?.query) { + this.clear(); + return; + } + + this.#searching.setValue(true); + this.#debouncedSearch(); + } + + /** + * Clear the current search query and result items. + * @memberof UmbPickerSearchManager + */ + public clear() { + this.#query.setValue(undefined); + this.#resultItems.setValue([]); + this.#searching.setValue(false); + this.#resultTotalItems.setValue(0); + } + + /** + * Set the search query. + * @param {QueryType} query The search query. + * @memberof UmbPickerSearchManager + */ + public setQuery(query: QueryType) { + if (this.getSearchable() === false) throw new Error('Search is not enabled'); + if (!this.query) { + this.clear(); + return; + } + + this.#query.setValue(query); + } + + /** + * Get the current search query. + * @memberof UmbPickerSearchManager + * @returns {QueryType | undefined} The current search query. + */ + public getQuery(): QueryType | undefined { + return this.#query.getValue(); + } + + /** + * Update the current search query. + * @param {Partial} query + * @memberof UmbPickerSearchManager + */ + public updateQuery(query: Partial) { + const mergedQuery = { ...this.getQuery(), ...query } as QueryType; + this.#query.setValue(mergedQuery); + } + + async #initSearch() { + const providerAlias = this.#config?.providerAlias; + if (!providerAlias) { + this.setSearchable(false); + throw new Error('No search provider alias provided'); + } + + this.#searchProvider = await createExtensionApiByAlias(this, providerAlias); + + if (!this.#searchProvider) { + this.setSearchable(false); + throw new Error(`Search Provider with alias ${providerAlias} is not available`); + } + + this.setSearchable(true); + } + + #debouncedSearch = debounce(this.#search, 300); + + async #search() { + if (this.getSearchable() === false) throw new Error('Search is not enabled'); + if (!this.#searchProvider) throw new Error('Search provider is not available'); + const query = this.#query.getValue(); + if (!query) throw new Error('No query provided'); + + if (!query.query) { + this.clear(); + return; + } + + const { data } = await this.#searchProvider.search(query); + const items = (data?.items as ResultItemType[]) ?? []; + this.#resultItems.setValue(items); + this.#resultTotalItems.setValue(data?.total ?? 0); + this.#searching.setValue(false); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/types.ts new file mode 100644 index 0000000000..9e4468e7fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manager/types.ts @@ -0,0 +1,3 @@ +export interface UmbPickerSearchManagerConfig { + providerAlias: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manifests.ts new file mode 100644 index 0000000000..2a13be78ab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as resultItemManifests } from './result-item/manifests.js'; +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...resultItemManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-field.element.ts new file mode 100644 index 0000000000..d178e701aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-field.element.ts @@ -0,0 +1,89 @@ +import type { UmbPickerContext } from '../picker.context.js'; +import { UMB_PICKER_CONTEXT } from '../picker.context.token.js'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { html, customElement, state, nothing, css } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; + +const elementName = 'umb-picker-search-field'; +@customElement(elementName) +export class UmbPickerSearchFieldElement extends UmbLitElement { + @state() + _query: string = ''; + + @state() + _searching: boolean = false; + + @state() + _isSearchable: boolean = false; + + #pickerContext?: UmbPickerContext; + + constructor() { + super(); + + this.consumeContext(UMB_PICKER_CONTEXT, (context) => { + this.#pickerContext = context; + this.observe(this.#pickerContext.search.searchable, (isSearchable) => (this._isSearchable = isSearchable)); + this.observe(this.#pickerContext.search.searching, (searching) => (this._searching = searching)); + this.observe(this.#pickerContext.search.query, (query) => (this._query = query?.query || '')); + }); + } + + #onInput(event: UUIInputEvent) { + const value = event.target.value as string; + this.#pickerContext?.search.updateQuery({ query: value }); + this.#pickerContext?.search.search(); + } + + override render() { + if (!this._isSearchable) return nothing; + + return html` + +
+ ${this._searching + ? html`` + : html``} +
+ + ${this._query + ? html` + this.#pickerContext?.search.clear()} compact> + + + ` + : nothing} +
+
+ `; + } + + static override readonly styles = [ + UmbTextStyles, + css` + uui-input { + width: 100%; + } + + #divider { + width: 100%; + height: 1px; + background-color: var(--uui-color-divider); + margin-top: var(--uui-size-space-5); + margin-bottom: var(--uui-size-space-3); + } + + #searching-indicator { + margin-left: 7px; + margin-top: 4px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPickerSearchFieldElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts new file mode 100644 index 0000000000..1b3a6411dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/picker-search-result.element.ts @@ -0,0 +1,72 @@ +import { UMB_PICKER_CONTEXT } from '../picker.context.token.js'; +import type { UmbPickerContext } from '../picker.context.js'; +import { customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { ManifestPickerSearchResultItem } from '@umbraco-cms/backoffice/extension-registry'; + +const elementName = 'umb-picker-search-result'; +@customElement(elementName) +export class UmbPickerSearchResultElement extends UmbLitElement { + @state() + _query?: UmbSearchRequestArgs; + + @state() + _searching: boolean = false; + + @state() + _items: UmbEntityModel[] = []; + + @state() + _isSearchable: boolean = false; + + #pickerContext?: UmbPickerContext; + + constructor() { + super(); + + this.consumeContext(UMB_PICKER_CONTEXT, (context) => { + this.#pickerContext = context; + this.observe(this.#pickerContext.search.searchable, (isSearchable) => (this._isSearchable = isSearchable)); + this.observe(this.#pickerContext.search.query, (query) => (this._query = query)); + this.observe(this.#pickerContext.search.searching, (query) => (this._searching = query)); + this.observe(this.#pickerContext.search.resultItems, (items) => (this._items = items)); + }); + } + + override render() { + if (!this._isSearchable) return nothing; + + if (this._query?.query && this._searching === false && this._items.length === 0) { + return this.#renderEmptyResult(); + } + + return html` + ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderResultItem(item), + )} + `; + } + + #renderEmptyResult() { + return html`No result for "${this._query?.query}".`; + } + + #renderResultItem(item: UmbEntityModel) { + return html` + manifest.forEntityTypes.includes(item.entityType)} + .elementProps=${{ item }}> + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbPickerSearchResultElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.context.token.ts new file mode 100644 index 0000000000..554e05cae7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbDefaultPickerSearchResultItemContext } from './default-picker-search-result-item.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_PICKER_SEARCH_RESULT_ITEM_CONTEXT = new UmbContextToken( + 'UmbPickerSearchResultItemContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.context.ts new file mode 100644 index 0000000000..7204b6679d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.context.ts @@ -0,0 +1,11 @@ +import { UMB_PICKER_SEARCH_RESULT_ITEM_CONTEXT } from './default-picker-search-result-item.context.token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbDefaultPickerSearchResultItemContext extends UmbContextBase { + constructor(host: UmbControllerHost) { + super(host, UMB_PICKER_SEARCH_RESULT_ITEM_CONTEXT); + } +} + +export { UmbDefaultPickerSearchResultItemContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.element.ts new file mode 100644 index 0000000000..f59d63e477 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/default-picker-search-result-item.element.ts @@ -0,0 +1,73 @@ +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPickerContext } from '@umbraco-cms/backoffice/picker'; +import { UMB_PICKER_CONTEXT } from '@umbraco-cms/backoffice/picker'; +import type { UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search'; + +const elementName = 'umb-default-picker-search-result-item'; +@customElement(elementName) +export class UmbDefaultPickerSearchResultItemElement extends UmbLitElement { + #item: UmbSearchResultItemModel | undefined; + @property({ type: Object }) + public get item(): UmbSearchResultItemModel | undefined { + return this.#item; + } + public set item(value: UmbSearchResultItemModel | undefined) { + this.#item = value; + this.#observeIsSelected(); + } + + @state() + _isSelected = false; + + #pickerContext?: UmbPickerContext; + + constructor() { + super(); + + this.consumeContext(UMB_PICKER_CONTEXT, (context) => { + this.#pickerContext = context; + this.#observeIsSelected(); + }); + } + + #observeIsSelected() { + const selectionManager = this.#pickerContext?.selection; + if (!selectionManager) return; + + const unique = this.item?.unique; + if (unique === undefined) return; + + this.observe(selectionManager.selection, () => { + this._isSelected = selectionManager.isSelected(unique); + }); + } + + override render() { + const item = this.item; + if (!item) return nothing; + + return html` + this.#pickerContext?.selection.select(item.unique)} + @deselected=${() => this.#pickerContext?.selection.deselect(item.unique)} + ?selected=${this._isSelected}> + + `; + } + + static override readonly styles = [UmbTextStyles]; +} + +export { UmbDefaultPickerSearchResultItemElement as element }; + +declare global { + interface HTMLElementTagNameMap { + [elementName]: UmbDefaultPickerSearchResultItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/manifests.ts new file mode 100644 index 0000000000..12711bdae6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/default/manifests.ts @@ -0,0 +1,15 @@ +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'kind', + alias: 'Umb.Kind.PickerSearchResultItem.Default', + matchKind: 'default', + matchType: 'pickerSearchResultItem', + manifest: { + type: 'pickerSearchResultItem', + api: () => import('./default-picker-search-result-item.context.js'), + element: () => import('./default-picker-search-result-item.element.js'), + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/manifests.ts new file mode 100644 index 0000000000..98e3dac659 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/search/result-item/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as defaultManifests } from './default/manifests.js'; +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...defaultManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker/types.ts new file mode 100644 index 0000000000..1d134b429b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker/types.ts @@ -0,0 +1,6 @@ +export interface UmbPickerContextConfig { + search?: { + providerAlias: string; + queryParams?: object; + }; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-type/workspace/property-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-type/workspace/property-type-workspace.context.ts index a5d949cb39..822676a46c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-type/workspace/property-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-type/workspace/property-type-workspace.context.ts @@ -172,6 +172,12 @@ export class UmbPropertyTypeWorkspaceContext | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string) { return this.#data.asObservablePart((data) => data?.[propertyAlias as keyof PropertyTypeData] as ReturnType); } @@ -180,6 +186,13 @@ export class UmbPropertyTypeWorkspaceContext} value - value can be a promise resolving into the actual value or the raw value it self. + * @returns {Promise} + * @description Set the value of this property. + */ async setPropertyValue(alias: string, value: unknown) { const currentData = this.#data.value; if (currentData) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts index 0527d11772..33fe7070bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-base-context.ts @@ -47,8 +47,10 @@ export class UmbPropertyDatasetContextBase } /** - * TODO: Write proper JSDocs here. - * @param propertyAlias + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. */ async propertyValueByAlias(propertyAlias: string) { return this.#values.asObservablePart((values) => { @@ -58,9 +60,11 @@ export class UmbPropertyDatasetContextBase } /** - * TODO: Write proper JSDocs here. - * @param alias - * @param value + * @function setPropertyValue + * @param {string} alias + * @param {PromiseLike} value - value can be a promise resolving into the actual value or the raw value it self. + * @returns {Promise} + * @description Set the value of this property. */ setPropertyValue(alias: string, value: unknown) { this.#values.appendOne({ alias, value }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts index 92527fa278..12cf856163 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts @@ -33,6 +33,5 @@ export interface UmbPropertyDatasetContext extends UmbContext { propertyValueByAlias( propertyAlias: string, ): Promise | undefined>; - // TODO: Append the andCulture method as well.. setPropertyValue(propertyAlias: string, value: unknown): void; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts index 69db32034b..f5485ac96e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts @@ -180,6 +180,7 @@ export class UmbPropertyContext extends UmbContextBase extends UmbModalBaseElement< + UmbTreePickerModalData, + UmbTreePickerModalValue +> { + @state() + _selectionConfiguration: UmbTreeSelectionConfiguration = { + multiple: false, + selectable: true, + selection: [], + }; + + @state() + _createPath?: string; + + @state() + _createLabel?: string; + + @state() + _searchQuery?: string; + + #pickerContext = new UmbTreeItemPickerContext(this); + + constructor() { + super(); + this.#pickerContext.selection.setSelectable(true); + this.#observePickerSelection(); + this.#observeSearch(); + } + + override connectedCallback(): void { + super.connectedCallback(); + this.#initCreateAction(); + } + + protected override async updated(_changedProperties: PropertyValueMap | Map) { + super.updated(_changedProperties); + + if (_changedProperties.has('data')) { + this.#pickerContext.search.updateConfig({ ...this.data?.search }); + + const multiple = this.data?.multiple ?? false; + this.#pickerContext.selection.setMultiple(multiple); + + this._selectionConfiguration = { + ...this._selectionConfiguration, + multiple, + }; + } + + if (_changedProperties.has('value')) { + const selection = this.value?.selection ?? []; + this.#pickerContext.selection.setSelection(selection); + this._selectionConfiguration = { + ...this._selectionConfiguration, + selection: [...selection], + }; + } + } + + #observePickerSelection() { + this.observe( + this.#pickerContext.selection.selection, + (selection) => { + this.updateValue({ selection }); + this.requestUpdate(); + }, + 'umbPickerSelectionObserver', + ); + } + + #observeSearch() { + this.observe( + this.#pickerContext.search.query, + (query) => { + this._searchQuery = query?.query; + }, + 'umbPickerSearchQueryObserver', + ); + } + + // Tree Selection + #onTreeItemSelected(event: UmbSelectedEvent) { + event.stopPropagation(); + 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.modalContext?.dispatchEvent(new UmbDeselectedEvent(event.unique)); + } + + // Create action + #initCreateAction() { + // TODO: If data.enableCreate is true, we should add a button to create a new item. [NL] + // Does the tree know enough about this, for us to be able to create a new item? [NL] + // I think we need to be able to get entityType and a parentId?, or do we only allow creation in the root? and then create via entity actions? [NL] + // To remove the hardcoded URLs for workspaces of entity types, we could make an create event from the tree, which either this or the sidebar impl. will pick up and react to. [NL] + // Or maybe the tree item context base can handle this? [NL] + // Maybe its a general item context problem to be solved. [NL] + const createAction = this.data?.createAction; + if (createAction) { + this._createLabel = createAction.label; + new UmbModalRouteRegistrationController( + this, + (createAction.modalToken as typeof UMB_WORKSPACE_MODAL) ?? UMB_WORKSPACE_MODAL, + ) + .onSetup(() => { + return { data: createAction.modalData }; + }) + .onSubmit((value) => { + if (value) { + this.value = { selection: [value.unique] }; + this._submitModal(); + } else { + this._rejectModal(); + } + }) + .observeRouteBuilder((routeBuilder) => { + const oldPath = this._createPath; + this._createPath = + routeBuilder({}) + createAction.extendWithPathPattern.generateLocal(createAction.extendWithPathParams); + this.requestUpdate('_createPath', oldPath); + }); + } + } + + override render() { + return html` + + ${this.#renderSearch()} ${this.#renderTree()} + ${this.#renderActions()} + + `; + } + #renderSearch() { + return html` + + + `; + } + + #renderTree() { + if (this._searchQuery) { + return nothing; + } + + return html` + + `; + } + + #renderActions() { + return html` +
+ + ${this._createPath + ? html` ` + : nothing} + +
+ `; + } +} + +export default UmbTreePickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tree-picker-modal': UmbTreePickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts deleted file mode 100644 index 29f9991636..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { UmbTreeSelectionConfiguration } from '../types.js'; -import type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker-modal.token.js'; -import { html, customElement, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UMB_WORKSPACE_MODAL, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; -import { UmbDeselectedEvent, UmbSelectedEvent, UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; -import type { UmbTreeElement, UmbTreeItemModelBase } from '@umbraco-cms/backoffice/tree'; - -@customElement('umb-tree-picker-modal') -export class UmbTreePickerModalElement extends UmbModalBaseElement< - UmbTreePickerModalData, - UmbTreePickerModalValue -> { - @state() - _selectionConfiguration: UmbTreeSelectionConfiguration = { - multiple: false, - selectable: true, - selection: [], - }; - - @state() - _createPath?: string; - - @state() - _createLabel?: string; - - override connectedCallback() { - super.connectedCallback(); - - // TODO: We should make a nicer way to observe the value.. [NL] - // This could be by observing when the modalCOntext gets set. [NL] - if (this.modalContext) { - this.observe(this.modalContext.value, (value) => { - this._selectionConfiguration.selection = value?.selection ?? []; - }); - } - - // Same here [NL] - this._selectionConfiguration.multiple = this.data?.multiple ?? false; - - // TODO: If data.enableCreate is true, we should add a button to create a new item. [NL] - // Does the tree know enough about this, for us to be able to create a new item? [NL] - // I think we need to be able to get entityType and a parentId?, or do we only allow creation in the root? and then create via entity actions? [NL] - // To remove the hardcoded URLs for workspaces of entity types, we could make an create event from the tree, which either this or the sidebar impl. will pick up and react to. [NL] - // Or maybe the tree item context base can handle this? [NL] - // Maybe its a general item context problem to be solved. [NL] - const createAction = this.data?.createAction; - if (createAction) { - this._createLabel = createAction.label; - new UmbModalRouteRegistrationController( - this, - (createAction.modalToken as typeof UMB_WORKSPACE_MODAL) ?? UMB_WORKSPACE_MODAL, - ) - .onSetup(() => { - return { data: createAction.modalData }; - }) - .onSubmit((value) => { - if (value) { - this.value = { selection: [value.unique] }; - this._submitModal(); - } else { - this._rejectModal(); - } - }) - .observeRouteBuilder((routeBuilder) => { - const oldPath = this._createPath; - this._createPath = - routeBuilder({}) + createAction.extendWithPathPattern.generateLocal(createAction.extendWithPathParams); - this.requestUpdate('_createPath', oldPath); - }); - } - } - - #onSelectionChange(event: UmbSelectionChangeEvent) { - event.stopPropagation(); - const element = event.target as UmbTreeElement; - this.value = { selection: element.getSelection() }; - this.modalContext?.dispatchEvent(new UmbSelectionChangeEvent()); - } - - #onSelected(event: UmbSelectedEvent) { - event.stopPropagation(); - this.modalContext?.dispatchEvent(new UmbSelectedEvent(event.unique)); - } - - #onDeselected(event: UmbDeselectedEvent) { - event.stopPropagation(); - this.modalContext?.dispatchEvent(new UmbDeselectedEvent(event.unique)); - } - - override render() { - return html` - - - - -
- - ${this._createPath - ? html` ` - : nothing} - -
-
- `; - } -} - -export default UmbTreePickerModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-tree-picker-modal': UmbTreePickerModalElement; - } -} 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 c04299b8e1..52da565908 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 @@ -36,6 +36,7 @@ export default defineConfig({ 'notification/index': './notification/index.ts', 'object-type/index': './object-type/index.ts', 'picker-input/index': './picker-input/index.ts', + 'picker/index': './picker/index.ts', 'property-action/index': './property-action/index.ts', 'property-editor/index': './property-editor/index.ts', 'property-type/index': './property-type/index.ts', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts index 46b0a136f8..fd70781bde 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-property-dataset/invariant-workspace-property-dataset-context.ts @@ -44,8 +44,10 @@ export class UmbInvariantWorkspacePropertyDatasetContext< } /** - * TODO: Write proper JSDocs here. - * @param propertyAlias + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. */ async propertyValueByAlias(propertyAlias: string) { return await this.#workspace.propertyValueByAlias(propertyAlias); diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts index 46bb5ec6a3..0f8bb744ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts @@ -344,6 +344,12 @@ export class UmbDataTypeWorkspaceContext this.#currentData.update({ editorUiAlias: alias }); } + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string) { await this.#getDataPromise; return this.#currentData.asObservablePart( diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts index de04460e28..857419aa90 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts @@ -251,7 +251,12 @@ export class UmbDocumentBlueprintWorkspaceContext async propertyStructureById(propertyId: string) { return this.structure.propertyStructureById(propertyId); } - + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string, variantId?: UmbVariantId) { return this.#currentData.asObservablePart( (data) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts index 9659dc6f9d..9ab4a1ac6d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts @@ -1,6 +1,10 @@ import { UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN } from '../paths.js'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -import type { UmbDocumentTypeTreeItemModel } from '@umbraco-cms/backoffice/document-type'; +import { + UMB_DOCUMENT_TYPE_ENTITY_TYPE, + UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, + type UmbDocumentTypeTreeItemModel, +} from '@umbraco-cms/backoffice/document-type'; import { type UmbTreePickerModalValue, type UmbTreePickerModalData, @@ -28,12 +32,12 @@ export const UMB_DOCUMENT_TYPE_PICKER_MODAL = new UmbModalToken< createAction: { label: '#content_createEmpty', modalData: { - entityType: 'document-type', + entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE, preset: {}, }, extendWithPathPattern: UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN, extendWithPathParams: { - parentEntityType: 'document-type-root', + parentEntityType: UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, parentUnique: null, presetAlias: null, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts new file mode 100644 index 0000000000..ec598f76cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/constants.ts @@ -0,0 +1 @@ +export const UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.DocumentType'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts index 8deaf580bd..503ee26f29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/search/manifests.ts @@ -1,10 +1,11 @@ import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../entity.js'; +import { UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS } from './constants.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { name: 'Document Type Search Provider', - alias: 'Umb.SearchProvider.DocumentType', + alias: UMB_DOCUMENT_TYPE_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./document-type.search-provider.js'), weight: 600, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts index 66981bdf87..3fb3e70336 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts @@ -4,6 +4,7 @@ import { manifests as entityBulkActionManifests } from './entity-bulk-actions/ma import { manifests as globalContextManifests } from './global-contexts/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as modalManifests } from './modals/manifests.js'; +import { manifests as pickerManifests } from './picker/manifests.js'; import { manifests as propertyEditorManifests } from './property-editors/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; @@ -12,15 +13,17 @@ import { manifests as trackedReferenceManifests } from './reference/manifests.js import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as userPermissionManifests } from './user-permissions/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ ...collectionManifests, ...entityActionManifests, ...entityBulkActionManifests, ...globalContextManifests, ...menuManifests, ...modalManifests, + ...pickerManifests, ...propertyEditorManifests, ...recycleBinManifests, ...repositoryManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts index b244d4093a..5fb0115f87 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts @@ -1,3 +1,4 @@ +import { UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS } from '../search/index.js'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; import { type UmbTreePickerModalValue, @@ -18,6 +19,9 @@ export const UMB_DOCUMENT_PICKER_MODAL = new UmbModalToken = [ + { + type: 'pickerSearchResultItem', + kind: 'default', + alias: 'Umb.PickerSearchResultItem.Document', + name: 'Document Picker Search Result Item', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index 7aa3d7932b..7b38600b9f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -2,7 +2,7 @@ import { UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS } from '../repository/index.j import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import { UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../entity.js'; -import { UMB_DOCUMENT_PICKER_MODAL } from '../../modals/document-picker-modal.token.js'; +import { UMB_DOCUMENT_PICKER_MODAL } from '../../modals/index.js'; import { UMB_USER_PERMISSION_DOCUMENT_DELETE, UMB_USER_PERMISSION_DOCUMENT_MOVE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts new file mode 100644 index 0000000000..13e085c832 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/constants.ts @@ -0,0 +1 @@ +export const UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Document'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts index cdc2e2e024..f9d8b8dce2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/search/manifests.ts @@ -1,10 +1,11 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; +import { UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS } from './constants.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { name: 'Document Search Provider', - alias: 'Umb.SearchProvider.Document', + alias: UMB_DOCUMENT_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./document.search-provider.js'), weight: 800, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 719e6d6719..1c6bc91860 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -364,6 +364,13 @@ export class UmbDocumentWorkspaceContext return this.structure.propertyStructureById(propertyId); } + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @param {UmbVariantId} variantId + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias( propertyAlias: string, variantId?: UmbVariantId, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts index 62f792f664..e9d9a3d2ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/manifests.ts @@ -3,9 +3,9 @@ import { manifests as documentBlueprintManifests } from './document-blueprints/m import { manifests as documentManifests } from './documents/manifests.js'; import { manifests as documentTypeManifests } from './document-types/manifests.js'; import { manifests as sectionManifests } from './section/manifests.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ +export const manifests: Array = [ ...dashboardManifests, ...documentBlueprintManifests, ...documentManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/manifests.ts new file mode 100644 index 0000000000..a879adb201 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/manifests.ts @@ -0,0 +1,20 @@ +import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../../entity.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + kind: 'default', + alias: 'Umb.EntityAction.MediaType.Export', + name: 'Export Media Type Entity Action', + forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE], + api: () => import('./media-type-export.action.js'), + meta: { + icon: 'icon-download-alt', + label: '#actions_export', + }, + }, +]; + +export const manifests: Array = [...entityActions, ...repositoryManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/media-type-export.action.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/media-type-export.action.ts new file mode 100644 index 0000000000..b6f7b4e539 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/media-type-export.action.ts @@ -0,0 +1,18 @@ +import { UmbExportMediaTypeRepository } from './repository/index.js'; +import { blobDownload } from '@umbraco-cms/backoffice/utils'; +import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; + +export class UmbExportMediaTypeEntityAction extends UmbEntityActionBase { + #repository = new UmbExportMediaTypeRepository(this); + + override async execute() { + if (!this.args.unique) throw new Error('Unique is not available'); + + const { data } = await this.#repository.requestExport(this.args.unique); + if (!data) return; + + blobDownload(data, `${this.args.unique}.udt`, 'text/xml'); + } +} + +export default UmbExportMediaTypeEntityAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/constants.ts new file mode 100644 index 0000000000..e248f4801e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/constants.ts @@ -0,0 +1 @@ +export const UMB_EXPORT_MEDIA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Export'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/index.ts new file mode 100644 index 0000000000..19252269f8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/index.ts @@ -0,0 +1,2 @@ +export { UmbExportMediaTypeRepository } from './media-type-export.repository.js'; +export { UMB_EXPORT_MEDIA_TYPE_REPOSITORY_ALIAS } from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/manifests.ts new file mode 100644 index 0000000000..e2b0c96286 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_EXPORT_MEDIA_TYPE_REPOSITORY_ALIAS } from './constants.js'; +import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const exportRepository: ManifestRepository = { + type: 'repository', + alias: UMB_EXPORT_MEDIA_TYPE_REPOSITORY_ALIAS, + name: 'Export Media Type Repository', + api: () => import('./media-type-export.repository.js'), +}; + +export const manifests: Array = [exportRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/media-type-export.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/media-type-export.repository.ts new file mode 100644 index 0000000000..8f8c65dbdf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/media-type-export.repository.ts @@ -0,0 +1,21 @@ +import { UmbExportMediaTypeServerDataSource } from './media-type-export.server.data-source.js'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbExportMediaTypeRepository extends UmbRepositoryBase { + #exportSource = new UmbExportMediaTypeServerDataSource(this); + + async requestExport(unique: string) { + const { data, error } = await this.#exportSource.export(unique); + + if (!error) { + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + const notification = { data: { message: `Exported` } }; + notificationContext.peek('positive', notification); + } + + return { data, error }; + } +} + +export { UmbExportMediaTypeRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/media-type-export.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/media-type-export.server.data-source.ts new file mode 100644 index 0000000000..7a23bc2fe4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/export/repository/media-type-export.server.data-source.ts @@ -0,0 +1,33 @@ +import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * Export Media Server Data Source + * @export + * @class UmbExportMediaTypeServerDataSource + */ +export class UmbExportMediaTypeServerDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbExportMediaTypeServerDataSource. + * @param {UmbControllerHost} host + * @memberof UmbExportMediaTypeServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Export an item for the given id to the destination unique + * @param {unique} unique + * @returns {*} + * @memberof UmbExportMediaTypeServerDataSource + */ + async export(unique: string) { + if (!unique) throw new Error('Unique is missing'); + + return tryExecuteAndNotify(this.#host, MediaTypeService.getMediaTypeByIdExport({ id: unique })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/manifests.ts new file mode 100644 index 0000000000..ccf24cd365 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/manifests.ts @@ -0,0 +1,21 @@ +import { UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE } from '../../entity.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as modalManifests } from './modal/manifests.js'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const entityActions: Array = [ + { + type: 'entityAction', + kind: 'default', + alias: 'Umb.EntityAction.MediaType.Import', + name: 'Export Media Type Entity Action', + forEntityTypes: [UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE], + api: () => import('./media-type-import.action.js'), + meta: { + icon: 'icon-page-up', + label: '#actions_import', + }, + }, +]; + +export const manifests: Array = [...entityActions, ...repositoryManifests, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/media-type-import.action.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/media-type-import.action.ts new file mode 100644 index 0000000000..ee1eaa956c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/media-type-import.action.ts @@ -0,0 +1,24 @@ +import { UMB_MEDIA_TYPE_IMPORT_MODAL } from './modal/media-type-import-modal.token.js'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbEntityActionBase, UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; + +export class UmbImportMediaTypeEntityAction extends UmbEntityActionBase { + override async execute() { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalContext = modalManager.open(this, UMB_MEDIA_TYPE_IMPORT_MODAL, { + data: { unique: this.args.unique }, + }); + await modalContext.onSubmit().catch(() => {}); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + unique: this.args.unique, + entityType: this.args.entityType, + }); + + actionEventContext.dispatchEvent(event); + } +} + +export default UmbImportMediaTypeEntityAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/index.ts new file mode 100644 index 0000000000..7d34d24e30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/index.ts @@ -0,0 +1 @@ +export * from './media-type-import-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/manifests.ts new file mode 100644 index 0000000000..71d2eebc8f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/manifests.ts @@ -0,0 +1,10 @@ +import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.MediaType.Import', + name: 'Media Type Import Modal', + element: () => import('./media-type-import-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts new file mode 100644 index 0000000000..76e40e994f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts @@ -0,0 +1,166 @@ +import { UmbMediaTypeImportRepository } from '../repository/media-type-import.repository.js'; +import type { UmbMediaTypeImportModalData, UmbMediaTypeImportModalValue } from './media-type-import-modal.token.js'; +import { css, html, customElement, query, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/media'; + +interface UmbMediaTypePreview { + unique: string; + name: string; + alias: string; + icon: string; +} + +@customElement('umb-media-type-import-modal') +export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< + UmbMediaTypeImportModalData, + UmbMediaTypeImportModalValue +> { + #MediaTypeImportRepository = new UmbMediaTypeImportRepository(this); + #temporaryUnique?: string; + #fileReader; + + @state() + private _fileContent: Array = []; + + @query('#dropzone') + private dropzone?: UmbDropzoneElement; + + constructor() { + super(); + this.#fileReader = new FileReader(); + this.#fileReader.onload = (e) => { + if (typeof e.target?.result === 'string') { + const fileContent = e.target.result; + this.#MediaTypePreviewBuilder(fileContent); + } else { + this.#requestReset(); + } + }; + } + + #onFileDropped() { + const data = this.dropzone?.getFiles()[0]; + if (!data) return; + + this.#temporaryUnique = data.temporaryUnique; + this.#fileReader.readAsText(data.file); + } + + async #onFileImport() { + if (!this.#temporaryUnique) return; + const { error } = await this.#MediaTypeImportRepository.requestImport(this.#temporaryUnique); + if (error) return; + this._submitModal(); + } + + #MediaTypePreviewBuilder(htmlString: string) { + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/xml'); + const childNodes = doc.childNodes; + + const elements: Array = []; + + childNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'MediaType') { + elements.push(node as Element); + } + }); + + this._fileContent = this.#MediaTypePreviewItemBuilder(elements); + } + + #MediaTypePreviewItemBuilder(elements: Array) { + const mediaTypes: Array = []; + elements.forEach((MediaType) => { + const info = MediaType.getElementsByTagName('Info')[0]; + const unique = info.getElementsByTagName('Key')[0].textContent ?? ''; + const name = info.getElementsByTagName('Name')[0].textContent ?? ''; + const alias = info.getElementsByTagName('Alias')[0].textContent ?? ''; + const icon = info.getElementsByTagName('Icon')[0].textContent ?? ''; + + mediaTypes.push({ unique, name, alias, icon }); + }); + return mediaTypes; + } + + #requestReset() { + this._fileContent = []; + this.#temporaryUnique = undefined; + } + + async #onBrowse() { + this.dropzone?.browse(); + } + + override render() { + return html` + ${this.#renderUploadZone()} + + + `; + } + + #renderUploadZone() { + return html` + ${when( + this._fileContent.length, + () => + html` + + + `, + () => + /**TODO Add localizations */ + html`
+ Drag and drop your file here + + +
`, + )} + `; + } + + static override styles = [ + UmbTextStyles, + css` + #wrapper { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + position: relative; + gap: var(--uui-size-space-3); + border: 2px dashed var(--uui-color-divider-standalone); + background-color: var(--uui-color-surface-alt); + padding: var(--uui-size-space-6); + } + + #import { + margin-top: var(--uui-size-space-6); + } + `, + ]; +} + +export default UmbMediaTypeImportModalLayout; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-type-import-modal': UmbMediaTypeImportModalLayout; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.token.ts new file mode 100644 index 0000000000..2b76b49a79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.token.ts @@ -0,0 +1,17 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMediaTypeImportModalData { + unique: string | null; +} + +export interface UmbMediaTypeImportModalValue {} + +export const UMB_MEDIA_TYPE_IMPORT_MODAL = new UmbModalToken( + 'Umb.Modal.MediaType.Import', + { + modal: { + type: 'sidebar', + size: 'small', + }, + }, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/constants.ts new file mode 100644 index 0000000000..a990f6ea73 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/constants.ts @@ -0,0 +1 @@ +export const UMB_MEDIA_TYPE_IMPORT_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Import'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/index.ts new file mode 100644 index 0000000000..0fe6ef6e20 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/index.ts @@ -0,0 +1,2 @@ +export { UmbMediaTypeImportRepository } from './media-type-import.repository.js'; +export { UMB_MEDIA_TYPE_IMPORT_REPOSITORY_ALIAS } from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/manifests.ts new file mode 100644 index 0000000000..d1dd143c9c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_MEDIA_TYPE_IMPORT_REPOSITORY_ALIAS } from './constants.js'; +import type { ManifestRepository, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const importRepository: ManifestRepository = { + type: 'repository', + alias: UMB_MEDIA_TYPE_IMPORT_REPOSITORY_ALIAS, + name: 'Import Media Type Repository', + api: () => import('./media-type-import.repository.js'), +}; + +export const manifests: Array = [importRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/media-type-import.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/media-type-import.repository.ts new file mode 100644 index 0000000000..1f13bbe4f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/media-type-import.repository.ts @@ -0,0 +1,21 @@ +import { UmbMediaTypeImportServerDataSource } from './media-type-import.server.data-source.js'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbMediaTypeImportRepository extends UmbRepositoryBase { + #importSource = new UmbMediaTypeImportServerDataSource(this); + + async requestImport(temporaryUnique: string) { + const { data, error } = await this.#importSource.import(temporaryUnique); + + if (!error) { + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + const notification = { data: { message: `Imported` } }; + notificationContext.peek('positive', notification); + } + + return { data, error }; + } +} + +export { UmbMediaTypeImportRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/media-type-import.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/media-type-import.server.data-source.ts new file mode 100644 index 0000000000..c71e44051d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/repository/media-type-import.server.data-source.ts @@ -0,0 +1,37 @@ +import { MediaTypeService, type PostMediaTypeImportData } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; + +/** + * Media Type Import Server Data Source + * @Import + * @class UmbMediaTypeImportServerDataSource + */ +export class UmbMediaTypeImportServerDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbMediaTypeImportServerDataSource. + * @param {UmbControllerHost} host + * @memberof UmbMediaTypeImportServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Import an item for the given id to the destination unique + * @param {temporaryUnique} temporaryUnique + * @returns {*} + * @memberof UmbMediaTypeImportServerDataSource + */ + async import(temporaryUnique: string) { + if (!temporaryUnique) throw new Error('Unique is missing'); + + const requestBody: PostMediaTypeImportData = { + requestBody: { file: { id: temporaryUnique } }, + }; + + return tryExecuteAndNotify(this.#host, MediaTypeService.postMediaTypeImport(requestBody)); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts index 2b726ae1a8..8304c96731 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts @@ -3,6 +3,8 @@ import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_ITEM_REPOSITORY_ import { manifests as createManifests } from './create/manifests.js'; import { manifests as moveManifests } from './move-to/manifests.js'; import { manifests as duplicateManifests } from './duplicate/manifests.js'; +import { manifests as exportManifests } from './export/manifests.js'; +import { manifests as importManifests } from './import/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ @@ -24,4 +26,6 @@ export const manifests: Array = [ ...createManifests, ...moveManifests, ...duplicateManifests, + ...exportManifests, + ...importManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index 6c5f34884b..dc22cf60a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -256,7 +256,13 @@ export class UmbMediaWorkspaceContext async propertyStructureById(propertyId: string) { return this.structure.propertyStructureById(propertyId); } - + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @param {UmbVariantId} variantId + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string, variantId?: UmbVariantId) { return this.#currentData.asObservablePart( (data) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts index 79492d90a2..c5d00784c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts @@ -2,12 +2,12 @@ import { manifests as memberGroupManifests } from './member-group/manifests.js'; import { manifests as memberManifests } from './member/manifests.js'; import { manifests as memberSectionManifests } from './member-section/manifests.js'; import { manifests as memberTypeManifests } from './member-type/manifests.js'; -import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import './member/components/index.js'; import './member-group/components/index.js'; -export const manifests: Array = [ +export const manifests: Array = [ ...memberGroupManifests, ...memberManifests, ...memberSectionManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts index e0c3ec8ed1..ec3dcca072 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts @@ -1,13 +1,12 @@ import { UmbMemberCollectionRepository } from '../../collection/index.js'; -import { UmbMemberSearchProvider } from '../../search/member.search-provider.js'; import type { UmbMemberDetailModel } from '../../types.js'; import type { UmbMemberItemModel } from '../../repository/index.js'; import type { UmbMemberPickerModalValue, UmbMemberPickerModalData } from './member-picker-modal.token.js'; -import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; -import { debounce, UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbCollectionItemPickerContext } from '@umbraco-cms/backoffice/collection'; @customElement('umb-member-picker-modal') export class UmbMemberPickerModalElement extends UmbModalBaseElement< @@ -18,35 +17,42 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< private _members: Array = []; @state() - private _searchQuery: string = ''; - - @state() - private _searchResult: Array = []; - - @state() - private _searching = false; + private _searchQuery?: string; #collectionRepository = new UmbMemberCollectionRepository(this); - #selectionManager = new UmbSelectionManager(this); - #searchProvider = new UmbMemberSearchProvider(this); - - override connectedCallback(): void { - super.connectedCallback(); - this.#selectionManager.setSelectable(true); - this.#selectionManager.setMultiple(this.data?.multiple ?? false); - this.#selectionManager.setSelection(this.value?.selection ?? []); - } + #pickerContext = new UmbCollectionItemPickerContext(this); constructor() { super(); this.observe( - this.#selectionManager.selection, + this.#pickerContext.selection.selection, (selection) => { this.updateValue({ selection }); this.requestUpdate(); }, 'umbSelectionObserver', ); + + this.observe( + this.#pickerContext.search.query, + (query) => { + this._searchQuery = query?.query; + }, + 'umbPickerSearchQueryObserver', + ); + } + + protected override async updated(_changedProperties: PropertyValueMap | Map) { + super.updated(_changedProperties); + + if (_changedProperties.has('data')) { + this.#pickerContext.search.updateConfig({ ...this.data?.search }); + this.#pickerContext.selection.setMultiple(this.data?.multiple ?? false); + } + + if (_changedProperties.has('value')) { + this.#pickerContext.selection.setSelection(this.value?.selection); + } } override async firstUpdated() { @@ -62,54 +68,24 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< } } - #onSearchInput(event: UUIInputEvent) { - const value = event.target.value as string; - this._searchQuery = value; - - if (!this._searchQuery) { - this._searchResult = []; - this._searching = false; - return; - } - - this._searching = true; - this.#debouncedSearch(); - } - - #debouncedSearch = debounce(this.#search, 300); - - async #search() { - if (!this._searchQuery) return; - const { data } = await this.#searchProvider.search({ query: this._searchQuery }); - this._searchResult = data?.items ?? []; - this._searching = false; - } - - #onSearchClear() { - this._searchQuery = ''; - this._searchResult = []; - } - override render() { return html` - ${this.#renderSearch()} ${this.#renderItems()} -
- this.modalContext?.reject()}> - this.modalContext?.submit()}> -
+ + + + ${this.#renderItems()} + ${this.#renderActions()}
`; } #renderItems() { - if (this._searchQuery) return nothing; + if (this._searchQuery) { + return nothing; + } + return html` ${repeat( this.#filteredMembers, @@ -119,86 +95,35 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< `; } - #renderSearch() { - return html` - -
- ${this._searching - ? html`` - : html``} -
- ${when( - this._searchQuery, - () => html` -
- - - -
- `, - )} -
-
- ${this.#renderSearchResult()} - `; - } - - #renderSearchResult() { - if (this._searchQuery && this._searching === false && this._searchResult.length === 0) { - return this.#renderEmptySearchResult(); - } - - return html` - ${repeat( - this._searchResult, - (item) => item.unique, - (item) => this.#renderMemberItem(item), - )} - `; - } - - #renderEmptySearchResult() { - return html`No result for "${this._searchQuery}".`; - } - #renderMemberItem(item: UmbMemberItemModel | UmbMemberDetailModel) { return html` this.#selectionManager.select(item.unique)} - @deselected=${() => this.#selectionManager.deselect(item.unique)} - ?selected=${this.#selectionManager.isSelected(item.unique)}> + @selected=${() => this.#pickerContext.selection.select(item.unique)} + @deselected=${() => this.#pickerContext.selection.deselect(item.unique)} + ?selected=${this.#pickerContext.selection.isSelected(item.unique)}> `; } - static override styles = [ - UmbTextStyles, - css` - #search-input { - width: 100%; - } + #renderActions() { + return html` +
+ this.modalContext?.reject()}> + this.modalContext?.submit()}> +
+ `; + } - #search-divider { - width: 100%; - height: 1px; - background-color: var(--uui-color-divider); - margin-top: var(--uui-size-space-5); - margin-bottom: var(--uui-size-space-3); - } - - #search-indicator { - margin-left: 7px; - margin-top: 4px; - } - `, - ]; + static override styles = [UmbTextStyles]; } export default UmbMemberPickerModalElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts index b77c28327f..8da183eefe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts @@ -1,9 +1,12 @@ import type { UmbMemberItemModel } from '../../repository/index.js'; +import { UMB_MEMBER_SEARCH_PROVIDER_ALIAS } from '../../search/constants.js'; +import type { UmbPickerModalSearchConfig } from '@umbraco-cms/backoffice/modal'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbMemberPickerModalData { multiple?: boolean; filter?: (member: UmbMemberItemModel) => boolean; + search?: UmbPickerModalSearchConfig; } export interface UmbMemberPickerModalValue { @@ -17,5 +20,10 @@ export const UMB_MEMBER_PICKER_MODAL = new UmbModalToken = [ +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ ...collectionManifests, ...entityActionManifests, ...memberPickerModalManifests, + ...pickerManifests, ...propertyEditorManifests, ...repositoryManifests, ...searchManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/picker/manifests.ts new file mode 100644 index 0000000000..45e2df590f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/picker/manifests.ts @@ -0,0 +1,12 @@ +import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; +import type { ManifestTypes, UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'pickerSearchResultItem', + kind: 'default', + alias: 'Umb.PickerSearchResultItem.Member', + name: 'Member Picker Search Result Item', + forEntityTypes: [UMB_MEMBER_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts new file mode 100644 index 0000000000..8cca6e6a79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/constants.ts @@ -0,0 +1 @@ +export const UMB_MEMBER_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.Member'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts index 9729f06efd..e6b5c3845e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/search/manifests.ts @@ -1,10 +1,11 @@ import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEMBER_SEARCH_PROVIDER_ALIAS } from './constants.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { name: 'Member Search Provider', - alias: 'Umb.SearchProvider.Member', + alias: UMB_MEMBER_SEARCH_PROVIDER_ALIAS, type: 'searchProvider', api: () => import('./member.search-provider.js'), weight: 300, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index 1e249d615a..df3323d2d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -227,6 +227,13 @@ export class UmbMemberWorkspaceContext return this.structure.propertyStructureById(propertyId); } + /** + * @function propertyValueByAlias + * @param {string} propertyAlias + * @param {UmbVariantId} variantId + * @returns {Promise | undefined>} + * @description Get an Observable for the value of this property. + */ async propertyValueByAlias(propertyAlias: string, variantId?: UmbVariantId) { return this.#currentData.asObservablePart( (data) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/text-box/property-editor-ui-text-box.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/text-box/property-editor-ui-text-box.element.ts index 4cac623483..c79609f036 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/text-box/property-editor-ui-text-box.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/text-box/property-editor-ui-text-box.element.ts @@ -13,7 +13,7 @@ type UuiInputTypeType = typeof UUIInputElement.prototype.type; @customElement('umb-property-editor-ui-text-box') export class UmbPropertyEditorUITextBoxElement - extends UmbFormControlMixin(UmbLitElement, undefined) + extends UmbFormControlMixin(UmbLitElement, undefined) implements UmbPropertyEditorUiElement { /** @@ -83,7 +83,7 @@ export class UmbPropertyEditorUITextBoxElement ?readonly=${this.readonly}>`; } - static styles = [ + static override styles = [ UmbTextStyles, css` uui-input { diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/search/types.ts index a1309314d4..c051328e18 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/search/types.ts @@ -1,6 +1,7 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import type { UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository'; +// TODO: lower requirement for search provider item type export type UmbSearchResultItemModel = { entityType: string; icon?: string | null; @@ -13,6 +14,9 @@ export type UmbSearchRequestArgs = { query: string; }; -export interface UmbSearchProvider extends UmbApi { - search(args: UmbSearchRequestArgs): Promise>>; +export interface UmbSearchProvider< + SearchResultItemType extends UmbSearchResultItemModel = UmbSearchResultItemModel, + SearchRequestArgsType extends UmbSearchRequestArgs = UmbSearchRequestArgs, +> extends UmbApi { + search(args: SearchRequestArgsType): Promise>>; } diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 715ff5c48d..13058d558f 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -54,6 +54,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/content-type": ["./src/packages/core/content-type/index.ts"], "@umbraco-cms/backoffice/content": ["./src/packages/core/content/index.ts"], "@umbraco-cms/backoffice/culture": ["./src/packages/core/culture/index.ts"], + "@umbraco-cms/backoffice/picker": ["./src/packages/core/picker/index.ts"], "@umbraco-cms/backoffice/current-user": ["./src/packages/user/current-user/index.ts"], "@umbraco-cms/backoffice/data-type": ["./src/packages/data-type/index.ts"], "@umbraco-cms/backoffice/debug": ["./src/packages/core/debug/index.ts"],