diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts new file mode 100644 index 0000000000..6ab13a8459 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-collection-data-source.ts @@ -0,0 +1,75 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbCollectionFilterModel, UmbCollectionItemModel } from '@umbraco-cms/backoffice/collection'; +import type { + UmbPickerCollectionDataSource, + UmbPickerSearchableDataSource, +} from '@umbraco-cms/backoffice/picker-data-source'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export class ExampleCustomPickerCollectionPropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerCollectionDataSource, UmbPickerSearchableDataSource +{ + async requestCollection(args: UmbCollectionFilterModel) { + // TODO: use args to filter/paginate etc + console.log(args); + const data = { + items: customItems, + total: customItems.length, + }; + + return { data }; + } + + async requestItems(uniques: Array) { + const items = customItems.filter((x) => uniques.includes(x.unique)); + return { data: items }; + } + + async search(args: UmbSearchRequestArgs) { + const items = customItems.filter((item) => item.name?.toLowerCase().includes(args.query.toLowerCase())); + const total = items.length; + + const data = { + items, + total, + }; + + return { data }; + } +} + +export { ExampleCustomPickerCollectionPropertyEditorDataSource as api }; + +const customItems: Array = [ + { + unique: '1', + entityType: 'example', + name: 'Example 1', + icon: 'icon-shape-triangle', + }, + { + unique: '2', + entityType: 'example', + name: 'Example 2', + icon: 'icon-shape-triangle', + }, + { + unique: '3', + entityType: 'example', + name: 'Example 3', + icon: 'icon-shape-triangle', + }, + { + unique: '4', + entityType: 'example', + name: 'Example 4', + icon: 'icon-shape-triangle', + }, + { + unique: '5', + entityType: 'example', + name: 'Example 5', + icon: 'icon-shape-triangle', + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-tree-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-tree-data-source.ts new file mode 100644 index 0000000000..8e6f8e6ce2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-custom-picker-tree-data-source.ts @@ -0,0 +1,126 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbPickerTreeDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { UmbTreeChildrenOfRequestArgs, UmbTreeItemModel } from '@umbraco-cms/backoffice/tree'; + +export class ExampleCustomPickerTreePropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerTreeDataSource +{ + async requestTreeRoot() { + const root = { + unique: null, + name: 'Examples', + icon: 'icon-folder', + hasChildren: true, + entityType: 'example-root', + isFolder: true, + }; + + return { data: root }; + } + + async requestTreeRootItems() { + // TODO: implement args when needed + const rootItems = customItems.filter((item) => item.parent.unique === null); + + const data = { + items: rootItems, + total: rootItems.length, + }; + + return { data }; + } + + async requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) { + const items = customItems.filter( + (item) => item.parent.entityType === args.parent.entityType && item.parent.unique === args.parent.unique, + ); + + const data = { + items: items, + total: items.length, + }; + + return { data }; + } + + async requestTreeItemAncestors() { + // TODO: implement when needed + return { data: [] }; + } + + async requestItems(uniques: Array) { + const items = customItems.filter((x) => uniques.includes(x.unique)); + return { data: items }; + } + + async search(args: UmbSearchRequestArgs) { + const result = customItems.filter((item) => item.name.toLowerCase().includes(args.query.toLowerCase())); + + const data = { + items: result, + totalItems: result.length, + }; + + return { data }; + } +} + +export { ExampleCustomPickerTreePropertyEditorDataSource as api }; + +const customItems: Array = [ + { + unique: '1', + entityType: 'example', + name: 'Example 1', + icon: 'icon-shape-triangle', + parent: { unique: null, entityType: 'example-root' }, + isFolder: false, + hasChildren: false, + }, + { + unique: '2', + entityType: 'example', + name: 'Example 2', + icon: 'icon-shape-triangle', + parent: { unique: null, entityType: 'example-root' }, + isFolder: false, + hasChildren: false, + }, + { + unique: '3', + entityType: 'example', + name: 'Example 3', + icon: 'icon-shape-triangle', + parent: { unique: null, entityType: 'example-root' }, + isFolder: false, + hasChildren: false, + }, + { + unique: '4', + entityType: 'example', + name: 'Example 4', + icon: 'icon-shape-triangle', + parent: { unique: '6', entityType: 'example-folder' }, + isFolder: false, + hasChildren: false, + }, + { + unique: '5', + entityType: 'example', + name: 'Example 5', + icon: 'icon-shape-triangle', + parent: { unique: '6', entityType: 'example-folder' }, + isFolder: false, + hasChildren: false, + }, + { + unique: '6', + entityType: 'example-folder', + name: 'Example Folder 1', + parent: { unique: null, entityType: 'example-root' }, + isFolder: true, + hasChildren: true, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts new file mode 100644 index 0000000000..68937ecce7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-document-picker-data-source.ts @@ -0,0 +1,75 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { + UmbDocumentItemRepository, + UmbDocumentSearchRepository, + UmbDocumentTreeRepository, + type UmbDocumentSearchRequestArgs, +} from '@umbraco-cms/backoffice/document'; +import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document-type'; +import type { + UmbPickerSearchableDataSource, + UmbPickerTreeDataSource, +} from '@umbraco-cms/backoffice/picker-data-source'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; +import { getConfigValue, type UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils'; + +export class ExampleDocumentPickerPropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerTreeDataSource, UmbPickerSearchableDataSource +{ + #tree = new UmbDocumentTreeRepository(this); + #item = new UmbDocumentItemRepository(this); + #search = new UmbDocumentSearchRepository(this); + #config: UmbConfigCollectionModel = []; + + setConfig(config: UmbConfigCollectionModel) { + // TODO: add examples for all config options + this.#config = config; + } + + getConfig(): UmbConfigCollectionModel { + return this.#config; + } + + requestTreeRoot() { + return this.#tree.requestTreeRoot(); + } + + requestTreeRootItems(args: UmbTreeRootItemsRequestArgs) { + return this.#tree.requestTreeRootItems(args); + } + + requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) { + return this.#tree.requestTreeItemsOf(args); + } + + requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) { + return this.#tree.requestTreeItemAncestors(args); + } + + requestItems(uniques: Array) { + return this.#item.requestItems(uniques); + } + + search(args: UmbSearchRequestArgs) { + const filterString = getConfigValue(this.#config, 'filter'); + const filterArray = filterString ? filterString.split(',') : []; + const allowedContentTypes: UmbDocumentSearchRequestArgs['allowedContentTypes'] = filterArray.map( + (unique: string) => ({ + entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE, + unique, + }), + ); + + const combinedArgs: UmbDocumentSearchRequestArgs = { ...args, allowedContentTypes }; + + return this.#search.search(combinedArgs); + } +} + +export { ExampleDocumentPickerPropertyEditorDataSource as api }; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-language-picker-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-language-picker-data-source.ts new file mode 100644 index 0000000000..867bccd9eb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-language-picker-data-source.ts @@ -0,0 +1,22 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; +import { UmbLanguageCollectionRepository, UmbLanguageItemRepository } from '@umbraco-cms/backoffice/language'; +import type { UmbPickerCollectionDataSource } from '@umbraco-cms/backoffice/picker-data-source'; + +export class ExampleLanguagePickerPropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerCollectionDataSource +{ + #collection = new UmbLanguageCollectionRepository(this); + #item = new UmbLanguageItemRepository(this); + + requestCollection(args: UmbCollectionFilterModel) { + return this.#collection.requestCollection(args); + } + + requestItems(uniques: Array) { + return this.#item.requestItems(uniques); + } +} + +export { ExampleLanguagePickerPropertyEditorDataSource as api }; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-media-picker-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-media-picker-data-source.ts new file mode 100644 index 0000000000..fe62ea3bfd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-media-picker-data-source.ts @@ -0,0 +1,51 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { + UmbMediaItemRepository, + UmbMediaSearchRepository, + UmbMediaTreeRepository, +} from '@umbraco-cms/backoffice/media'; +import type { + UmbPickerSearchableDataSource, + UmbPickerTreeDataSource, +} from '@umbraco-cms/backoffice/picker-data-source'; +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; + +export class ExampleMediaPickerPropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerTreeDataSource, UmbPickerSearchableDataSource +{ + #tree = new UmbMediaTreeRepository(this); + #item = new UmbMediaItemRepository(this); + #search = new UmbMediaSearchRepository(this); + + requestTreeRoot() { + return this.#tree.requestTreeRoot(); + } + + requestTreeRootItems(args: UmbTreeRootItemsRequestArgs) { + return this.#tree.requestTreeRootItems(args); + } + + requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) { + return this.#tree.requestTreeItemsOf(args); + } + + requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) { + return this.#tree.requestTreeItemAncestors(args); + } + + requestItems(uniques: Array) { + return this.#item.requestItems(uniques); + } + + search(args: UmbSearchRequestArgs) { + return this.#search.search(args); + } +} + +export { ExampleMediaPickerPropertyEditorDataSource as api }; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-user-picker-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-user-picker-data-source.ts new file mode 100644 index 0000000000..b5985c0a77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-user-picker-data-source.ts @@ -0,0 +1,22 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; +import type { UmbPickerCollectionDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import { UmbUserCollectionRepository, UmbUserItemRepository } from '@umbraco-cms/backoffice/user'; + +export class ExampleUserPickerPropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerCollectionDataSource +{ + #collection = new UmbUserCollectionRepository(this); + #item = new UmbUserItemRepository(this); + + requestCollection(args: UmbCollectionFilterModel) { + return this.#collection.requestCollection(args); + } + + requestItems(uniques: Array) { + return this.#item.requestItems(uniques); + } +} + +export { ExampleUserPickerPropertyEditorDataSource as api }; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-webhook-picker-data-source.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-webhook-picker-data-source.ts new file mode 100644 index 0000000000..65070168f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/example-webhook-picker-data-source.ts @@ -0,0 +1,22 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; +import type { UmbPickerCollectionDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import { UmbWebhookCollectionRepository, UmbWebhookItemRepository } from '@umbraco-cms/backoffice/webhook'; + +export class ExampleWebhookPickerPropertyEditorDataSource + extends UmbControllerBase + implements UmbPickerCollectionDataSource +{ + #collection = new UmbWebhookCollectionRepository(this); + #item = new UmbWebhookItemRepository(this); + + requestCollection(args: UmbCollectionFilterModel) { + return this.#collection.requestCollection(args); + } + + requestItems(uniques: Array) { + return this.#item.requestItems(uniques); + } +} + +export { ExampleWebhookPickerPropertyEditorDataSource as api }; diff --git a/src/Umbraco.Web.UI.Client/examples/picker-data-source/index.ts b/src/Umbraco.Web.UI.Client/examples/picker-data-source/index.ts new file mode 100644 index 0000000000..fd4d9f3a2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/picker-data-source/index.ts @@ -0,0 +1,102 @@ +export const manifests: Array = [ + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.CustomPickerCollection', + name: 'Custom Picker Collection Data Source', + api: () => import('./example-custom-picker-collection-data-source.js'), + meta: { + label: 'Example Items (Collection)', + icon: 'icon-list', + description: 'Pick example items from a collection', + }, + }, + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.CustomPickerTree', + name: 'Custom Picker Tree Data Source', + api: () => import('./example-custom-picker-tree-data-source.js'), + meta: { + label: 'Example Items (Tree)', + icon: 'icon-tree', + description: 'Pick example items from a tree', + }, + }, + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.DocumentPicker', + name: 'Document Picker Data Source', + api: () => import('./example-document-picker-data-source.js'), + meta: { + label: 'Documents', + icon: 'icon-document', + description: 'Pick a document', + settings: { + properties: [ + { + alias: 'startNode', + label: 'Node type', + description: '', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.ContentPicker.Source', + }, + { + alias: 'filter', + label: 'Allow items of type', + description: 'Select the applicable types', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.ContentPicker.SourceType', + }, + ], + }, + }, + }, + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.MediaPicker', + name: 'Media Picker Data Source', + api: () => import('./example-media-picker-data-source.js'), + meta: { + label: 'Media', + icon: 'icon-document-image', + description: 'Pick a media item', + }, + }, + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.LanguagePicker', + name: 'Language Picker Data Source', + api: () => import('./example-language-picker-data-source.js'), + meta: { + label: 'Languages', + icon: 'icon-globe', + description: 'Pick a language', + }, + }, + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.WebhookPicker', + name: 'Webhook Picker Data Source', + api: () => import('./example-webhook-picker-data-source.js'), + meta: { + label: 'Webhooks', + icon: 'icon-webhook', + description: 'Pick a webhook', + }, + }, + { + type: 'propertyEditorDataSource', + dataSourceType: 'picker', + alias: 'Umb.PropertyEditorDataSource.UserPicker', + name: 'User Picker Data Source', + api: () => import('./example-user-picker-data-source.js'), + meta: { + label: 'Users', + icon: 'icon-user', + description: 'Pick a user', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 1d27a7f499..c7805ee9ac 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -3658,10 +3658,6 @@ "resolved": "src/external/rxjs", "link": true }, - "node_modules/@umbraco-backoffice/search": { - "resolved": "src/packages/search", - "link": true - }, "node_modules/@umbraco-backoffice/segment": { "resolved": "src/packages/segment", "link": true @@ -17101,7 +17097,8 @@ "name": "@umbraco-backoffice/rte" }, "src/packages/search": { - "name": "@umbraco-backoffice/search" + "name": "@umbraco-backoffice/search", + "extraneous": true }, "src/packages/segment": { "name": "@umbraco-backoffice/segment" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index da7b4d3dab..ca5cc8853a 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -76,14 +76,17 @@ "./menu": "./dist-cms/packages/core/menu/index.js", "./modal": "./dist-cms/packages/core/modal/index.js", "./models": "./dist-cms/packages/core/models/index.js", + "./search": "./dist-cms/packages/core/search/index.js", "./multi-url-picker": "./dist-cms/packages/multi-url-picker/index.js", "./notification": "./dist-cms/packages/core/notification/index.js", "./object-type": "./dist-cms/packages/core/object-type/index.js", "./package": "./dist-cms/packages/packages/package/index.js", "./partial-view": "./dist-cms/packages/templating/partial-views/index.js", "./picker-input": "./dist-cms/packages/core/picker-input/index.js", + "./picker-data-source": "./dist-cms/packages/core/picker-data-source/index.js", "./picker": "./dist-cms/packages/core/picker/index.js", "./property-action": "./dist-cms/packages/core/property-action/index.js", + "./property-editor-data-source": "./dist-cms/packages/core/property-editor-data-source/index.js", "./property-editor": "./dist-cms/packages/core/property-editor/index.js", "./property-type": "./dist-cms/packages/content/property-type/index.js", "./property": "./dist-cms/packages/core/property/index.js", @@ -95,7 +98,6 @@ "./router": "./dist-cms/packages/core/router/index.js", "./rte": "./dist-cms/packages/rte/index.js", "./script": "./dist-cms/packages/templating/scripts/index.js", - "./search": "./dist-cms/packages/search/index.js", "./section": "./dist-cms/packages/core/section/index.js", "./segment": "./dist-cms/packages/segment/index.js", "./server-file-system": "./dist-cms/packages/core/server-file-system/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts index b284635d58..558792cabc 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts @@ -37,7 +37,6 @@ const CORE_PACKAGES = [ import('../../packages/publish-cache/umbraco-package.js'), import('../../packages/relations/umbraco-package.js'), import('../../packages/rte/umbraco-package.js'), - import('../../packages/search/umbraco-package.js'), import('../../packages/settings/umbraco-package.js'), import('../../packages/static-file/umbraco-package.js'), import('../../packages/sysinfo/umbraco-package.js'), diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-custom-view-guide/block-type-custom-view-guide.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-custom-view-guide/block-type-custom-view-guide.element.ts index 40cc20ba8c..d76496870e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-custom-view-guide/block-type-custom-view-guide.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-custom-view-guide/block-type-custom-view-guide.element.ts @@ -37,9 +37,8 @@ export class UmbBlockTypeCustomViewGuideElement extends UmbLitElement { await context?.propertyValueByAlias('contentElementTypeKey'), async (value) => { if (!value) return; - const { asObservable } = await this.#repository.requestByUnique(value); this.observe( - asObservable(), + (await this.#repository.requestByUnique(value)).asObservable?.(), (model) => { this.#contentTypeName = model?.name; this.#contentTypeAlias = model?.alias; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/components/property-type-based-property/property-type-based-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/components/property-type-based-property/property-type-based-property.element.ts index fea5d1fa32..aeff988144 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/components/property-type-based-property/property-type-based-property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/components/property-type-based-property/property-type-based-property.element.ts @@ -54,9 +54,11 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { private _propertyEditorSchemaAlias?: string; @state() - private _isUnsupported?: boolean; + private _propertyEditorDataSourceAlias?: string; @state() + private _isUnsupported?: boolean; + private _dataTypeValues?: UmbPropertyEditorConfig; private _dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); @@ -89,6 +91,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { this._dataTypeValues = dataType?.values; this._propertyEditorUiAlias = dataType?.editorUiAlias || undefined; this._propertyEditorSchemaAlias = dataType?.editorAlias || undefined; + this._propertyEditorDataSourceAlias = dataType?.editorDataSourceAlias || undefined; this._checkSchemaSupport(); // If there is no UI, we will look up the Property editor model to find the default UI alias: @@ -128,6 +131,7 @@ export class UmbPropertyTypeBasedPropertyElement extends UmbLitElement { .description=${this._property.description ?? undefined} .appearance=${this._property.appearance} property-editor-ui-alias=${ifDefined(this._propertyEditorUiAlias)} + property-editor-data-source-alias=${ifDefined(this._propertyEditorDataSourceAlias)} .config=${this._dataTypeValues} .validation=${this._property.validation} ?readonly=${this.readonly}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/collection-item-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/collection-item-picker-modal.element.ts new file mode 100644 index 0000000000..f9081af885 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/collection-item-picker-modal.element.ts @@ -0,0 +1,154 @@ +import type { UmbCollectionSelectionConfiguration } from '../types.js'; +import { UmbCollectionItemPickerContext } from './collection-item-picker-modal.context.js'; +import type { UmbCollectionItemPickerModalData, UmbCollectionItemPickerModalValue } from './types.js'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, state, nothing, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; + +@customElement('umb-collection-item-picker-modal') +export class UmbCollectionItemPickerModalElement extends UmbModalBaseElement< + UmbCollectionItemPickerModalData, + UmbCollectionItemPickerModalValue +> { + @state() + private _selectionConfiguration: UmbCollectionSelectionConfiguration = { + multiple: false, + selectable: true, + selection: [], + }; + + @state() + private _hasSelection: boolean = false; + + @state() + private _searchQuery?: string; + + #pickerContext = new UmbCollectionItemPickerContext(this); + + constructor() { + super(); + this.#pickerContext.selection.setSelectable(true); + this.observe(this.#pickerContext.selection.hasSelection, (hasSelection) => { + this._hasSelection = hasSelection; + }); + this.#observePickerSelection(); + this.#observeSearch(); + } + + protected override async updated(_changedProperties: PropertyValueMap | Map) { + super.updated(_changedProperties); + + if (_changedProperties.has('data')) { + if (this.data?.search) { + 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', + ); + } + + #onItemSelected(event: UmbSelectedEvent) { + event.stopPropagation(); + this.#pickerContext.selection.select(event.unique); + this.modalContext?.dispatchEvent(new UmbSelectedEvent(event.unique)); + } + + #onItemDeselected(event: UmbDeselectedEvent) { + event.stopPropagation(); + this.#pickerContext.selection.deselect(event.unique); + this.modalContext?.dispatchEvent(new UmbDeselectedEvent(event.unique)); + } + + override render() { + return html` + + ${this.#renderSearch()} ${this.#renderCollection()} + ${this.#renderActions()} + + `; + } + #renderSearch() { + return html` + + + `; + } + + #renderCollection() { + if (this._searchQuery) { + return nothing; + } + + return html` `; + } + + #renderActions() { + return html` +
+ + +
+ `; + } +} + +export { UmbCollectionItemPickerModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-collection-item-picker-modal': UmbCollectionItemPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/constants.ts new file mode 100644 index 0000000000..aed0c8bcc2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/constants.ts @@ -0,0 +1 @@ +export const UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS = 'Umb.Modal.CollectionItemPicker'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/manifests.ts new file mode 100644 index 0000000000..c06fb671c7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'modal', + alias: UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS, + name: 'Collection Item Picker Modal', + element: () => import('./collection-item-picker-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/types.ts new file mode 100644 index 0000000000..88317726bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-item-picker-modal/types.ts @@ -0,0 +1,15 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; + +export interface UmbCollectionItemPickerModalData + extends UmbPickerModalData { + collection: UmbCollectionItemPickerModalCollectionConfig; +} + +export interface UmbCollectionItemPickerModalCollectionConfig> { + menuAlias: string; + filterArgs?: FilterArgsType; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbCollectionItemPickerModalValue extends UmbPickerModalValue {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/constants.ts new file mode 100644 index 0000000000..90c7f72dc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/constants.ts @@ -0,0 +1,2 @@ +export * from './collection-item-picker-modal/constants.js'; +export * from './menu/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts new file mode 100644 index 0000000000..d567a5dbd8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/global-components.ts @@ -0,0 +1,3 @@ +import './menu/collection-menu.element.js'; + +export * from './menu/collection-menu.element.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 7ffdd86cf6..5f32ec4ac1 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 @@ -2,11 +2,13 @@ import './default/collection-default.element.js'; import './collection.element.js'; import './components/index.js'; -export * from './default/collection-default.element.js'; +export * from './collection-item-picker-modal/index.js'; export * from './collection.element.js'; export * from './components/index.js'; export * from './conditions/index.js'; -export * from './collection-item-picker-modal/index.js'; +export * from './constants.js'; +export * from './default/collection-default.element.js'; +export * from './global-components.js'; export * from './workspace-view/index.js'; export * from './default/collection-default.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts new file mode 100644 index 0000000000..528282ebdb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/item/types.ts @@ -0,0 +1,7 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbCollectionItemModel extends UmbEntityModel { + unique: string; + name?: string; + icon?: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts index c59c5fa9f4..ee82008f76 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/manifests.ts @@ -1,10 +1,14 @@ import type { UmbExtensionManifestKind } from '../extension-registry/registry.js'; -import { manifests as conditionManifests } from './conditions/manifests.js'; import { manifests as actionManifests } from './action/manifests.js'; +import { manifests as conditionManifests } from './conditions/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; +import { manifests as pickerModalManifests } from './collection-item-picker-modal/manifests.js'; import { manifests as workspaceViewManifests } from './workspace-view/manifests.js'; export const manifests: Array = [ ...actionManifests, - ...workspaceViewManifests, ...conditionManifests, + ...menuManifests, + ...pickerModalManifests, + ...workspaceViewManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/collection-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/collection-menu.element.ts new file mode 100644 index 0000000000..7058b09161 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/collection-menu.element.ts @@ -0,0 +1,27 @@ +import type { ManifestCollectionMenu } from './extension/types.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbExtensionElementAndApiSlotElementBase } from '@umbraco-cms/backoffice/extension-registry'; + +@customElement('umb-collection-menu') +export class UmbCollectionMenuElement extends UmbExtensionElementAndApiSlotElementBase { + getExtensionType() { + return 'collectionMenu'; + } + + getDefaultElementName() { + return 'umb-default-collection-menu'; + } + + getSelection() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // TODO: make base interface for a collection menu element + return this._element?.getSelection?.() ?? []; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-collection-menu': UmbCollectionMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts new file mode 100644 index 0000000000..5b0fcb8cb6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/constants.ts @@ -0,0 +1 @@ +export { UMB_COLLECTION_MENU_CONTEXT } from './default/default-collection-menu.context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.context.token.ts new file mode 100644 index 0000000000..2ee0654be7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.context.token.ts @@ -0,0 +1,6 @@ +import type { UmbDefaultCollectionMenuContext } from './default-collection-menu.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_COLLECTION_MENU_CONTEXT = new UmbContextToken( + 'UmbCollectionMenuContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.context.ts new file mode 100644 index 0000000000..de32faad25 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.context.ts @@ -0,0 +1,123 @@ +import type { ManifestCollectionMenu } from '../extension/types.js'; +import type { UmbCollectionRepository } from '../../repository/index.js'; +import type { UmbCollectionItemModel } from '../../item/types.js'; +import { UMB_COLLECTION_MENU_CONTEXT } from './default-collection-menu.context.token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbPaginationManager, UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; + +export class UmbDefaultCollectionMenuContext extends UmbContextBase { + public selectableFilter?: (item: UmbCollectionItemModel) => boolean = () => true; + public filter?: (item: UmbCollectionItemModel) => boolean = () => true; + public filterArgs?: Record; + + public readonly selection = new UmbSelectionManager(this); + public readonly pagination = new UmbPaginationManager(); + + #items = new UmbArrayState([], (x) => x.unique); + items = this.#items.asObservable(); + + #manifest?: ManifestCollectionMenu; + #repository?: UmbCollectionRepository; + + #paging = { + skip: 0, + take: 50, + }; + + #initResolver?: () => void; + #initialized = false; + + #init = new Promise((resolve) => { + if (this.#initialized) { + resolve(); + } else { + this.#initResolver = resolve; + } + }); + + constructor(host: UmbControllerHost) { + super(host, UMB_COLLECTION_MENU_CONTEXT); + + this.pagination.setPageSize(this.#paging.take); + //this.#consumeContexts(); + + // listen for page changes on the pagination manager + this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange); + + // always load the tree root because we need the root entity to reload the entire tree + this.#loadItems(); + } + + /** + * Sets the manifest + * @param {ManifestTree} manifest + * @memberof UmbDefaultTreeContext + */ + public set manifest(manifest: ManifestCollectionMenu | undefined) { + if (this.#manifest === manifest) return; + this.#manifest = manifest; + this.#observeRepository(this.#manifest?.meta.collectionRepositoryAlias); + } + public get manifest() { + return this.#manifest; + } + + #checkIfInitialized() { + if (this.#repository) { + this.#initialized = true; + this.#initResolver?.(); + } + } + + #onPageChange = (event: UmbChangeEvent) => { + const target = event.target as UmbPaginationManager; + this.#paging.skip = target.getSkip(); + this.#loadItems(true); + }; + + async #loadItems(loadMore = false) { + await this.#init; + + const skip = loadMore ? this.#paging.skip : 0; + const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take; + + const { data } = await this.#repository!.requestCollection({ + ...this.filterArgs, + skip, + take, + }); + + if (data) { + if (loadMore) { + const currentItems = this.#items.getValue(); + this.#items.setValue([...currentItems, ...data.items]); + } else { + this.#items.setValue(data.items); + } + + this.pagination.setTotalItems(data.total); + } + } + + #observeRepository(repositoryAlias?: string) { + if (!repositoryAlias) throw new Error('Collection Menu must have a repository alias.'); + + new UmbExtensionApiInitializer>( + this, + umbExtensionsRegistry, + repositoryAlias, + [this], + (permitted, ctrl) => { + this.#repository = permitted ? ctrl.api : undefined; + this.#checkIfInitialized(); + }, + ); + } +} + +export { UmbDefaultCollectionMenuContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts new file mode 100644 index 0000000000..a1273792f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/default-collection-menu.element.ts @@ -0,0 +1,162 @@ +import type { UmbCollectionItemModel } from '../../item/types.js'; +import type { UmbCollectionSelectionConfiguration } from '../../types.js'; +import type { UmbDefaultCollectionMenuContext } from './default-collection-menu.context.js'; +import { getItemFallbackIcon, getItemFallbackName } from '@umbraco-cms/backoffice/entity-item'; +import { + html, + customElement, + property, + type PropertyValueMap, + state, + repeat, + nothing, + css, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-default-collection-menu') +export class UmbDefaultCollectionMenuElement extends UmbLitElement { + private _api: UmbDefaultCollectionMenuContext | undefined; + @property({ type: Object, attribute: false }) + public get api(): UmbDefaultCollectionMenuContext | undefined { + return this._api; + } + public set api(value: UmbDefaultCollectionMenuContext | undefined) { + this._api = value; + + if (this._api) { + this._api.filterArgs = this.#filterArgs; + } + + this.#observeData(); + } + + private _selectionConfiguration: UmbCollectionSelectionConfiguration = { + multiple: false, + selectable: true, + selection: [], + }; + @property({ type: Object, attribute: false }) + selectionConfiguration: UmbCollectionSelectionConfiguration = this._selectionConfiguration; + + @property({ attribute: false }) + selectableFilter: (item: UmbCollectionItemModel) => boolean = () => true; + + @property({ attribute: false }) + filter: (item: UmbCollectionItemModel) => boolean = () => true; + + public get filterArgs(): Record | undefined { + return this.#filterArgs; + } + public set filterArgs(value: Record | undefined) { + this.#filterArgs = value; + + if (this._api) { + this._api.filterArgs = this.#filterArgs; + } + } + + #filterArgs: Record | undefined; + + @state() + private _items: Array = []; + + @state() + private _currentPage = 1; + + @state() + private _totalPages = 1; + + #observeData() { + this.observe(this._api?.items, (items) => (this._items = items ?? [])); + this.observe(this._api?.pagination.currentPage, (value) => (this._currentPage = value ?? 1)); + this.observe(this._api?.pagination.totalPages, (value) => (this._totalPages = value ?? 1)); + } + + protected override async updated( + _changedProperties: PropertyValueMap | Map, + ): Promise { + super.updated(_changedProperties); + if (this._api === undefined) return; + + if (_changedProperties.has('selectionConfiguration')) { + this._selectionConfiguration = this.selectionConfiguration; + + this._api!.selection.setMultiple(this._selectionConfiguration.multiple ?? false); + this._api!.selection.setSelectable(this._selectionConfiguration.selectable ?? true); + this._api!.selection.setSelection(this._selectionConfiguration.selection ?? []); + } + + if (_changedProperties.has('selectableFilter')) { + this._api!.selectableFilter = this.selectableFilter; + } + + if (_changedProperties.has('filter')) { + this._api!.filter = this.filter; + } + } + + #onLoadMoreClick = (event: any) => { + event.stopPropagation(); + const next = (this._currentPage = this._currentPage + 1); + this._api?.pagination.setCurrentPageNumber(next); + }; + + override render() { + return html` + ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + )} + ${this.#renderPaging()} + `; + } + + #renderItem(item: UmbCollectionItemModel) { + return html` + this._api?.selection.select(item.unique)} + @deselected=${() => this._api?.selection.deselect(item.unique)} + ?selected=${this._api?.selection.isSelected(item.unique)}> + ${item.icon + ? html`` + : html``} + + `; + } + + #renderPaging() { + if (this._totalPages <= 1 || this._currentPage === this._totalPages) { + return nothing; + } + + return html` `; + } + + static override styles = css` + :host { + --uui-menu-item-indent: 0; + --uui-menu-item-flat-structure: 1; + } + + #load-more { + width: 100%; + } + `; +} + +export { UmbDefaultCollectionMenuElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-default-collection-menu': UmbDefaultCollectionMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/index.ts new file mode 100644 index 0000000000..2400048918 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/index.ts @@ -0,0 +1,2 @@ +export { UMB_COLLECTION_MENU_CONTEXT as UMB_COLLECTION_CONTEXT } from './default-collection-menu.context.token.js'; +export { UmbDefaultCollectionMenuContext } from './default-collection-menu.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/manifests.ts new file mode 100644 index 0000000000..6c2f94e287 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/default/manifests.ts @@ -0,0 +1,16 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'kind', + alias: 'Umb.Kind.CollectionMenu.Default', + matchKind: 'default', + matchType: 'collectionMenu', + manifest: { + type: 'collectionMenu', + kind: 'default', + element: () => import('./default-collection-menu.element.js'), + api: () => import('./default-collection-menu.context.js'), + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/extension/collection-menu.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/extension/collection-menu.extension.ts new file mode 100644 index 0000000000..b61540c7e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/extension/collection-menu.extension.ts @@ -0,0 +1,16 @@ +import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestCollectionMenu extends ManifestElementAndApi { + type: 'collectionMenu'; + meta: MetaCollectionMenu; +} + +export interface MetaCollectionMenu { + collectionRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + UmbCollectionMenu: ManifestCollectionMenu; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/extension/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/extension/types.ts new file mode 100644 index 0000000000..fc15649134 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/extension/types.ts @@ -0,0 +1 @@ +export type * from './collection-menu.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/manifests.ts new file mode 100644 index 0000000000..43d020c74e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as defaultManifests } from './default/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [...defaultManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/types.ts new file mode 100644 index 0000000000..90c1bd6808 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/menu/types.ts @@ -0,0 +1 @@ +export type * from './extension/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index f76d9ef226..67fb479e88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -4,9 +4,12 @@ import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; import type { UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; export type * from './action/create/types.js'; -export type * from './extensions/types.js'; export type * from './conditions/types.js'; +export type * from './extensions/types.js'; +export type * from './item/types.js'; +export type * from './menu/types.js'; export type * from './workspace-view/types.js'; +export type * from './collection-item-picker-modal/types.js'; export interface UmbCollectionConfiguration { unique?: UmbEntityUnique; @@ -33,6 +36,12 @@ export interface UmbCollectionLayoutConfiguration { collectionView: string; } +export type UmbCollectionSelectionConfiguration = { + multiple?: boolean; + selectable?: boolean; + selection?: Array; +}; + export interface UmbCollectionContext { setConfig(config: UmbCollectionConfiguration): void; getConfig(): UmbCollectionConfiguration | undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts index 5946030eb4..1c4a68b727 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts @@ -5,8 +5,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; -import type { UmbUniqueItemModel } from '@umbraco-cms/backoffice/models'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { getItemFallbackName, type UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; @customElement('umb-input-entity') export class UmbInputEntityElement extends UmbFormControlMixin( @@ -60,7 +60,7 @@ export class UmbInputEntityElement extends UmbFormControlMixin string; + getIcon?: (item: UmbItemModel) => string; @property({ type: String, attribute: 'min-message' }) maxMessage = 'This field exceeds the allowed amount of items'; @@ -93,7 +93,7 @@ export class UmbInputEntityElement extends UmbFormControlMixin; + private _items?: Array; #pickerContext?: UmbPickerInputContext; @@ -133,7 +133,7 @@ export class UmbInputEntityElement extends UmbFormControlMixin + ${when(icon, () => html``)} this.#removeItem(item)} label=${this.localize.term('general_remove')}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/default-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/default-item-ref.element.ts index 44c750fe70..e4355dd70d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/default-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/default-item-ref.element.ts @@ -1,11 +1,12 @@ -import type { UmbDefaultItemModel } from '../types.js'; +import type { UmbItemModel } from '../types.js'; +import { getItemFallbackIcon, getItemFallbackName } from '../utils.js'; import { customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-default-item-ref') export class UmbDefaultItemRefElement extends UmbLitElement { @property({ type: Object }) - item?: UmbDefaultItemModel; + item?: UmbItemModel; @property({ type: Boolean }) standalone = false; @@ -14,16 +15,19 @@ export class UmbDefaultItemRefElement extends UmbLitElement { if (!this.item) return nothing; return html` - + ${this.#renderIcon(this.item)} `; } - #renderIcon(item: UmbDefaultItemModel) { - if (!item.icon) return; - return html``; + #renderIcon(item: UmbItemModel) { + const icon = item.icon || getItemFallbackIcon(); + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts index 37c47e5ec3..985ccb3b2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts @@ -1,4 +1,7 @@ export * from './item-data-api-get-request-controller/index.js'; export * from './entity-item-ref/index.js'; +export * from './utils.js'; + +export type { UmbItemModel } from './types.js'; export type { UmbItemDataResolver, UmbItemDataResolverConstructor } from './data-resolver/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts index 3c778ff06b..573e05a7a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts @@ -2,6 +2,10 @@ import type { UmbEntityModel, UmbNamedEntityModel } from '@umbraco-cms/backoffic export type * from './item-data-api-get-request-controller/types.js'; export type * from './data-resolver/types.js'; +// TODO: v19 - remove +/** + * @deprecated - Deprecated since v17. Will be removed in v19. Use UmbItemModel instead. + */ export interface UmbDefaultItemModel extends UmbNamedEntityModel { icon?: string; } @@ -9,5 +13,5 @@ export interface UmbDefaultItemModel extends UmbNamedEntityModel { export interface UmbItemModel extends UmbEntityModel { unique: string; name?: string; - icon?: string; + icon?: string | null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/utils.ts new file mode 100644 index 0000000000..d2778809fe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/utils.ts @@ -0,0 +1,18 @@ +import type { UmbItemModel } from './types.js'; + +/** + * Returns a fallback name for an item + * @param {UmbItemModel} item The item to get the fallback name for + * @returns A fallback name + */ +export function getItemFallbackName(item: UmbItemModel): string { + return `${item.entityType}:${item.unique}`; +} + +/** + * Returns a fallback icon for an item + * @returns A fallback icon + */ +export function getItemFallbackIcon(): string { + return 'icon-circle-dotted'; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts index 09b197e06e..a4ee26214f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts @@ -8,11 +8,13 @@ import { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; import { UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; import type { UmbEntryPointOnInit } from '@umbraco-cms/backoffice/extension-api'; -import './property-action/components/index.js'; -import './menu/components/index.js'; -import './extension-registry/components/index.js'; +import './collection/global-components.js'; import './entity-item/global-components.js'; import './entity-sign/components/index.js'; +import './extension-registry/components/index.js'; +import './menu/components/index.js'; +import './property-action/components/index.js'; +import './property-editor-data-source/global-components.js'; export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => { new UmbExtensionsApiInitializer(host, extensionRegistry, 'globalContext', [host]); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts index 95f219131a..03ac8a8bbd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts @@ -1,7 +1,7 @@ import { umbExtensionsRegistry } from './registry.js'; import { property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; +import type { ClassConstructor, ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; import { UmbExtensionElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; export abstract class UmbExtensionElementAndApiSlotElementBase< @@ -42,6 +42,8 @@ export abstract class UmbExtensionElementAndApiSlotElementBase< abstract getExtensionType(): string; abstract getDefaultElementName(): string; + public getDefaultApiConstructor?(): ClassConstructor; + #observeManifest() { if (!this.alias) return; @@ -52,6 +54,7 @@ export abstract class UmbExtensionElementAndApiSlotElementBase< [this], this.#extensionChanged, this.getDefaultElementName(), + this.getDefaultApiConstructor?.(), ); this.#extensionController.elementProps = this.props; } 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 4cccf89abb..bd889e2d9d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts @@ -13,9 +13,11 @@ import { manifests as menuManifests } from './menu/manifests.js'; import { manifests as modalManifests } from './modal/manifests.js'; import { manifests as pickerManifests } from './picker/manifests.js'; import { manifests as propertyActionManifests } from './property-action/manifests.js'; +import { manifests as propertyEditorDataSourceManifests } from './property-editor-data-source/manifests.js'; import { manifests as propertyEditorManifests } from './property-editor/manifests.js'; import { manifests as propertyManifests } from './property/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; +import { manifests as searchManifests } from './search/manifests.js'; import { manifests as sectionManifests } from './section/manifests.js'; import { manifests as serverFileSystemManifests } from './server-file-system/manifests.js'; import { manifests as temporaryFileManifests } from './temporary-file/manifests.js'; @@ -41,9 +43,11 @@ export const manifests: Array = ...modalManifests, ...pickerManifests, ...propertyActionManifests, + ...propertyEditorDataSourceManifests, ...propertyEditorManifests, ...propertyManifests, ...recycleBinManifests, + ...searchManifests, ...sectionManifests, ...serverFileSystemManifests, ...temporaryFileManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/collection-data-source/is-picker-collection-data-source.guard.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/collection-data-source/is-picker-collection-data-source.guard.ts new file mode 100644 index 0000000000..827a51dcf3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/collection-data-source/is-picker-collection-data-source.guard.ts @@ -0,0 +1,9 @@ +import type { UmbPickerCollectionDataSource } from './types.js'; + +/** + * + * @param dataSource + */ +export function isPickerCollectionDataSource(dataSource: unknown): dataSource is UmbPickerCollectionDataSource { + return (dataSource as UmbPickerCollectionDataSource).requestCollection !== undefined; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/collection-data-source/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/collection-data-source/types.ts new file mode 100644 index 0000000000..c8ef834453 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/collection-data-source/types.ts @@ -0,0 +1,5 @@ +import type { UmbPickerDataSource } from '../data-source/types.js'; +import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; + +export interface UmbPickerCollectionDataSource extends UmbPickerDataSource, UmbCollectionRepository, UmbApi {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/data-source/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/data-source/types.ts new file mode 100644 index 0000000000..ee184a09b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/data-source/types.ts @@ -0,0 +1,8 @@ +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import type { UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils'; + +export interface UmbPickerDataSource extends UmbItemRepository, UmbApi { + setConfig?(config: UmbConfigCollectionModel | undefined): void; + getConfig?(): UmbConfigCollectionModel | undefined; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/index.ts new file mode 100644 index 0000000000..ca0e12ded8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/index.ts @@ -0,0 +1,4 @@ +export * from './collection-data-source/is-picker-collection-data-source.guard.js'; +export * from './searchable-data-source/is-picker-searchable.data-source.guard.js'; +export * from './tree-data-source/is-picker-tree-data-source.guard.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/searchable-data-source/is-picker-searchable.data-source.guard.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/searchable-data-source/is-picker-searchable.data-source.guard.ts new file mode 100644 index 0000000000..b502978b39 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/searchable-data-source/is-picker-searchable.data-source.guard.ts @@ -0,0 +1,9 @@ +import type { UmbPickerSearchableDataSource } from './types.js'; + +/** + * + * @param dataSource + */ +export function isPickerSearchableDataSource(dataSource: unknown): dataSource is UmbPickerSearchableDataSource { + return (dataSource as UmbPickerSearchableDataSource).search !== undefined; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/searchable-data-source/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/searchable-data-source/types.ts new file mode 100644 index 0000000000..9baee08655 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/searchable-data-source/types.ts @@ -0,0 +1,4 @@ +import type { UmbPickerDataSource } from '../types.js'; +import type { UmbSearchRepository } from '@umbraco-cms/backoffice/search'; + +export interface UmbPickerSearchableDataSource extends UmbPickerDataSource, UmbSearchRepository {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/tree-data-source/is-picker-tree-data-source.guard.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/tree-data-source/is-picker-tree-data-source.guard.ts new file mode 100644 index 0000000000..e8ba157335 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/tree-data-source/is-picker-tree-data-source.guard.ts @@ -0,0 +1,13 @@ +import type { UmbPickerTreeDataSource } from './types.js'; + +/** + * + * @param dataSource + */ +export function isPickerTreeDataSource(dataSource: unknown): dataSource is UmbPickerTreeDataSource { + return ( + (dataSource as UmbPickerTreeDataSource).requestTreeRoot !== undefined && + (dataSource as UmbPickerTreeDataSource).requestTreeRootItems !== undefined && + (dataSource as UmbPickerTreeDataSource).requestTreeItemsOf !== undefined + ); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/tree-data-source/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/tree-data-source/types.ts new file mode 100644 index 0000000000..ce5cb9d63b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/tree-data-source/types.ts @@ -0,0 +1,5 @@ +import type { UmbPickerDataSource } from '../data-source/types.js'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbTreeRepository } from '@umbraco-cms/backoffice/tree'; + +export interface UmbPickerTreeDataSource extends UmbPickerDataSource, UmbTreeRepository, UmbApi {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/types.ts new file mode 100644 index 0000000000..573c9dbca2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-data-source/types.ts @@ -0,0 +1,4 @@ +export type * from './collection-data-source/types.js'; +export type * from './searchable-data-source/types.js'; +export type * from './tree-data-source/types.js'; +export type * from './data-source/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index e3d1483b99..eb86ba1879 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -12,16 +12,15 @@ import { type UmbPickerModalData, type UmbPickerModalValue, } from '@umbraco-cms/backoffice/modal'; - -type PickerItemBaseType = { name: string; unique: string }; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; export class UmbPickerInputContext< - PickedItemType extends PickerItemBaseType = PickerItemBaseType, - PickerItemType extends PickerItemBaseType = PickedItemType, + PickedItemType extends UmbItemModel = UmbItemModel, + PickerItemType extends UmbItemModel = UmbItemModel, PickerModalConfigType extends UmbPickerModalData = UmbPickerModalData, PickerModalValueType extends UmbPickerModalValue = UmbPickerModalValue, > extends UmbContextBase { - modalAlias: string | UmbModalToken, PickerModalValueType>; + modalAlias?: string | UmbModalToken, PickerModalValueType>; repository?: UmbItemRepository; #itemManager; @@ -33,6 +32,7 @@ export class UmbPickerInputContext< /** * Define a minimum amount of selected items in this input, for this input to be valid. + * @returns {number} The minimum number of items required. */ public get max() { return this._max; @@ -44,6 +44,7 @@ export class UmbPickerInputContext< /** * Define a maximum amount of selected items in this input, for this input to be valid. + * @returns {number} The minimum number of items required. */ public get min() { return this._min; @@ -63,10 +64,13 @@ export class UmbPickerInputContext< constructor( host: UmbControllerHost, repositoryAlias: string, - modalAlias: string | UmbModalToken, PickerModalValueType>, + modalAlias?: string | UmbModalToken, PickerModalValueType>, ) { super(host, UMB_PICKER_INPUT_CONTEXT); - this.modalAlias = modalAlias; + + if (modalAlias) { + this.modalAlias = modalAlias; + } this.#itemManager = new UmbRepositoryItemsManager(this, repositoryAlias); @@ -86,6 +90,11 @@ export class UmbPickerInputContext< async openPicker(pickerData?: Partial) { await this.#itemManager.init; + + if (!this.modalAlias) { + throw new Error('No modal alias defined for the picker input context.'); + } + const modalValue = await umbOpenModal(this, this.modalAlias, { data: { multiple: this._max === 1 ? false : true, 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 index 4fcbe790de..242d58b00a 100644 --- 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 @@ -1,12 +1,14 @@ import { UMB_PICKER_CONTEXT } from '../picker.context.token.js'; import type { UmbPickerContext } from '../picker.context.js'; +import { UmbDefaultPickerSearchResultItemElement } from './result-item/default/default-picker-search-result-item.element.js'; import type { ManifestPickerSearchResultItem } from './result-item/picker-search-result-item.extension.js'; +import { UmbDefaultPickerSearchResultItemContext } from './result-item/default/default-picker-search-result-item.context.js'; import { customElement, html, nothing, property, 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 { UmbSearchRequestArgs, UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; -type PickableFilterMethodType = (item: T) => boolean; +type PickableFilterMethodType = (item: T) => boolean; @customElement('umb-picker-search-result') export class UmbPickerSearchResultElement extends UmbLitElement { @@ -17,7 +19,7 @@ export class UmbPickerSearchResultElement extends UmbLitElement { private _searching: boolean = false; @state() - private _items: UmbEntityModel[] = []; + private _items: UmbItemModel[] = []; @state() private _isSearchable: boolean = false; @@ -67,7 +69,7 @@ export class UmbPickerSearchResultElement extends UmbLitElement { return html`No result for "${this._query?.query}".`; } - #renderResultItem(item: UmbEntityModel) { + #renderResultItem(item: UmbSearchResultItemModel) { return html` + }} + .fallbackRenderMethod=${() => this.#renderFallbackResultItem(item)}> `; } + + #renderFallbackResultItem(item: UmbSearchResultItemModel) { + const element = new UmbDefaultPickerSearchResultItemElement(); + element.item = item; + element.disabled = this.pickableFilter ? !this.pickableFilter(item) : undefined; + new UmbDefaultPickerSearchResultItemContext(element); + return element; + } } declare global { 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 index a936ff88d7..6cc772b24e 100644 --- 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 @@ -1,4 +1,5 @@ import { UmbPickerSearchResultItemElementBase } from '../picker-search-result-item-element-base.js'; +import { getItemFallbackIcon, getItemFallbackName } from '@umbraco-cms/backoffice/entity-item'; import { customElement, html, nothing } from '@umbraco-cms/backoffice/external/lit'; import type { UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search'; @@ -9,9 +10,9 @@ export class UmbDefaultPickerSearchResultItemElement extends UmbPickerSearchResu if (!item) return nothing; return html` extends UmbLitElement { +export abstract class UmbPickerSearchResultItemElementBase extends UmbLitElement { #item: ItemType | undefined; protected pickerContext?: UmbPickerContext; @@ -50,7 +50,7 @@ export abstract class UmbPickerSearchResultItemElementBase +{ + async getCollection(args: UmbPropertyEditorDataSourceCollectionFilterModel) { + const extensions = umbExtensionsRegistry.getByType('propertyEditorDataSource'); + + const extensionsWithAllowedDataSourceTypes = extensions.filter((ext) => { + if (args.dataSourceTypes && args.dataSourceTypes.length > 0) { + return args.dataSourceTypes.includes(ext.dataSourceType); + } + return true; + }); + + const filtered = extensionsWithAllowedDataSourceTypes.filter((manifest) => + manifest.name.toLowerCase().includes(args.filter?.toLowerCase() ?? ''), + ); + + const extensionsOrderedByLabel = filtered.sort((a, b) => a.name.localeCompare(b.name)); + + const skip = args.skip ?? 0; + const take = args.take ?? 100; + + const paged = extensionsOrderedByLabel.slice(skip, skip + take); + + const items: Array = paged.map((manifest) => ({ + entityType: UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE, + unique: manifest.alias, + name: manifest.meta.label ?? manifest.name, + icon: manifest.meta.icon ?? 'icon-box', + })); + + return { + data: { + items, + total: filtered.length, + }, + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/collection.repository.ts new file mode 100644 index 0000000000..31e1e85f05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/collection.repository.ts @@ -0,0 +1,17 @@ +import { UmbPropertyEditorDataSourceCollectionExtensionRegistryDataSource } from './collection.extension-registry.data-source.js'; +import type { UmbPropertyEditorDataSourceCollectionFilterModel } from './types.js'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; + +export class UmbPropertyEditorDataSourceCollectionRepository + extends UmbRepositoryBase + implements UmbCollectionRepository +{ + #collectionSource = new UmbPropertyEditorDataSourceCollectionExtensionRegistryDataSource(this); + + async requestCollection(filter: UmbPropertyEditorDataSourceCollectionFilterModel) { + return this.#collectionSource.getCollection(filter); + } +} + +export { UmbPropertyEditorDataSourceCollectionRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/constants.ts new file mode 100644 index 0000000000..a19c686c08 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/constants.ts @@ -0,0 +1,2 @@ +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_REPOSITORY_ALIAS = + 'Umb.Repository.PropertyEditorDataSourceCollection'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/index.ts new file mode 100644 index 0000000000..3fe6f285aa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/index.ts @@ -0,0 +1,2 @@ +export { UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_REPOSITORY_ALIAS } from './constants.js'; +export { UmbPropertyEditorDataSourceCollectionRepository } from './collection.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/manifests.ts new file mode 100644 index 0000000000..44c18ffa01 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_REPOSITORY_ALIAS, + name: 'Property Editor Data Source Collection Repository', + api: () => import('./collection.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/types.ts new file mode 100644 index 0000000000..d77deee224 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/data/types.ts @@ -0,0 +1,20 @@ +import type { + UmbCollectionDataSource, + UmbCollectionFilterModel, + UmbCollectionItemModel, +} from '@umbraco-cms/backoffice/collection'; + +export type UmbLanguageCollectionDataSource = UmbCollectionDataSource< + UmbPropertyEditorDataSourceCollectionItemModel, + UmbPropertyEditorDataSourceCollectionFilterModel +>; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbPropertyEditorDataSourceCollectionItemModel extends UmbCollectionItemModel { + unique: string; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbPropertyEditorDataSourceCollectionFilterModel extends UmbCollectionFilterModel { + dataSourceTypes?: Array; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/index.ts new file mode 100644 index 0000000000..1894eb8a5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/index.ts @@ -0,0 +1 @@ +export * from './data/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/manifests.ts new file mode 100644 index 0000000000..b336d2e822 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as dataManifests } from './data/manifests.js'; +import { manifests as menuManifests } from './menu/manifests.js'; + +export const manifests: Array = [...dataManifests, ...menuManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/menu/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/menu/constants.ts new file mode 100644 index 0000000000..46ea9699d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/menu/constants.ts @@ -0,0 +1 @@ +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_MENU_ALIAS = 'Umb.CollectionMenu.PropertyEditorDataSource'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/menu/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/menu/manifests.ts new file mode 100644 index 0000000000..61aadf347c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/menu/manifests.ts @@ -0,0 +1,15 @@ +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_REPOSITORY_ALIAS } from '../data/constants.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_MENU_ALIAS } from './constants.js'; +import type { ManifestCollectionMenu } from '@umbraco-cms/backoffice/collection'; + +export const manifests: Array = [ + { + type: 'collectionMenu', + kind: 'default', + name: 'Property Editor Data Source Collection Menu', + alias: UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_MENU_ALIAS, + meta: { + collectionRepositoryAlias: UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_REPOSITORY_ALIAS, + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/types.ts new file mode 100644 index 0000000000..185b39805d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/collection/types.ts @@ -0,0 +1 @@ +export type * from './data/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/constants.ts new file mode 100644 index 0000000000..24c976d964 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/constants.ts @@ -0,0 +1,4 @@ +export * from './collection/constants.js'; +export * from './item/constants.js'; +export * from './entity.js'; +export * from './search/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/entity.ts new file mode 100644 index 0000000000..b1a1587ce8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/entity.ts @@ -0,0 +1,3 @@ +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE = 'property-editor-data-source'; + +export type UmbPropertyEditorDataSourceEntityType = typeof UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/extension/property-editor-data-source.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/extension/property-editor-data-source.extension.ts new file mode 100644 index 0000000000..27b2c7aedb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/extension/property-editor-data-source.extension.ts @@ -0,0 +1,22 @@ +import type { PropertyEditorSettings } from '../../property-editor/extensions/types.js'; +import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api'; + +// TODO: base ManifestApiType on dataSourceType +export interface ManifestPropertyEditorDataSource extends ManifestApi { + type: 'propertyEditorDataSource'; + dataSourceType: string; + meta: MetaPropertyEditorDataSource; +} + +export interface MetaPropertyEditorDataSource { + label: string; + description?: string; + icon?: string; + settings?: PropertyEditorSettings; +} + +declare global { + interface UmbExtensionManifestMap { + UmbPropertyEditorDataSource: ManifestPropertyEditorDataSource; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/extension/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/extension/types.ts new file mode 100644 index 0000000000..220c886b41 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/extension/types.ts @@ -0,0 +1 @@ +export type * from './property-editor-data-source.extension.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/global-components.ts new file mode 100644 index 0000000000..e2c4befc69 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/global-components.ts @@ -0,0 +1,3 @@ +import './input/input-property-editor-data-source.element.js'; + +export { UmbInputPropertyEditorDataSourceElement } from './input/input-property-editor-data-source.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/index.ts new file mode 100644 index 0000000000..60262374d5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/index.ts @@ -0,0 +1,4 @@ +export * from './collection/index.js'; +export * from './constants.js'; +export * from './global-components.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/input/input-property-editor-data-source.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/input/input-property-editor-data-source.context.ts new file mode 100644 index 0000000000..fd18b160b9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/input/input-property-editor-data-source.context.ts @@ -0,0 +1,67 @@ +import type { UmbPropertyEditorDataSourceItemModel } from '../item/types.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_REPOSITORY_ALIAS } from '../item/constants.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_MENU_ALIAS } from '../collection/constants.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_SEARCH_PROVIDER_ALIAS } from '../search/constants.js'; +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { + UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS, + type UmbCollectionItemPickerModalData, + type UmbCollectionItemPickerModalValue, +} from '@umbraco-cms/backoffice/collection'; + +const modalToken = new UmbModalToken( + UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS, + { + modal: { + type: 'sidebar', + size: 'small', + }, + }, +); + +export class UmbPropertyEditorDataSourcePickerInputContext extends UmbPickerInputContext< + UmbPropertyEditorDataSourceItemModel, + UmbPropertyEditorDataSourceItemModel, + UmbCollectionItemPickerModalData, + UmbCollectionItemPickerModalValue +> { + constructor(host: UmbControllerHost) { + super(host, UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_REPOSITORY_ALIAS, modalToken); + } + + #dataSourceTypes: Array = []; + + setDataSourceTypes(value: Array) { + this.#dataSourceTypes = value; + } + + getDataSourceTypes(): Array { + return this.#dataSourceTypes; + } + + override async openPicker( + pickerData?: Partial>, + ) { + const combinedPickerData: Partial> = { + ...pickerData, + collection: { + menuAlias: UMB_PROPERTY_EDITOR_DATA_SOURCE_COLLECTION_MENU_ALIAS, + filterArgs: { + dataSourceTypes: this.getDataSourceTypes(), + ...pickerData?.collection?.filterArgs, + }, + }, + search: { + providerAlias: UMB_PROPERTY_EDITOR_DATA_SOURCE_SEARCH_PROVIDER_ALIAS, + queryParams: { + dataSourceTypes: this.getDataSourceTypes(), + ...pickerData?.search?.queryParams, + }, + }, + }; + + await super.openPicker(combinedPickerData); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/input/input-property-editor-data-source.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/input/input-property-editor-data-source.element.ts new file mode 100644 index 0000000000..9687e685c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/input/input-property-editor-data-source.element.ts @@ -0,0 +1,233 @@ +import type { UmbPropertyEditorDataSourceItemModel } from '../item/data/types.js'; + +import { UmbPropertyEditorDataSourcePickerInputContext } from './input-property-editor-data-source.context.js'; +import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit'; +import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; + +@customElement('umb-input-property-editor-data-source') +export class UmbInputPropertyEditorDataSourceElement extends UUIFormControlMixin(UmbLitElement, '') { + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => { + return element.id; + }, + getUniqueOfModel: (modelEntry) => { + return modelEntry; + }, + identifier: 'Umb.SorterIdentifier.InputPropertyEditorDataSource', + itemSelector: 'uui-ref-node', + containerSelector: 'uui-ref-list', + onChange: ({ model }) => { + this.selection = model; + this.dispatchEvent(new UmbChangeEvent()); + }, + }); + + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default + */ + @property({ type: Number }) + public set min(value: number) { + this.#pickerInputContext.min = value; + } + public get min(): number { + return this.#pickerInputContext.min; + } + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default + */ + @property({ type: Number }) + public set max(value: number) { + this.#pickerInputContext.max = value; + } + public get max(): number { + return this.#pickerInputContext.max; + } + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'max-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + @property({ type: Array }) + public set selection(uniques: Array) { + this.#pickerInputContext.setSelection(uniques); + this.#sorter.setModel(uniques); + } + public get selection(): Array { + return this.#pickerInputContext.getSelection(); + } + + @property() + public override set value(uniques: string) { + this.selection = splitStringToArray(uniques); + } + public override get value(): string { + return this.selection.join(','); + } + + /** + * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. + * @type {boolean} + * @attr + * @default + */ + @property({ type: Boolean, reflect: true }) + public get readonly() { + return this.#readonly; + } + public set readonly(value) { + this.#readonly = value; + + if (this.#readonly) { + this.#sorter.disable(); + } else { + this.#sorter.enable(); + } + } + #readonly = false; + + @property({ type: Array, attribute: 'data-source-types' }) + public set dataSourceTypes(value: Array) { + this.#pickerInputContext.setDataSourceTypes(value); + } + public get dataSourceTypes(): Array { + return this.#pickerInputContext.getDataSourceTypes(); + } + + @state() + private _items: Array = []; + + @state() + private _statuses?: Array; + + #pickerInputContext = new UmbPropertyEditorDataSourcePickerInputContext(this); + + constructor() { + super(); + + this.addValidator( + 'rangeUnderflow', + () => this.minMessage, + () => !!this.min && this.#pickerInputContext.getSelection().length < this.min, + ); + + this.addValidator( + 'rangeOverflow', + () => this.maxMessage, + () => !!this.max && this.#pickerInputContext.getSelection().length > this.max, + ); + + this.observe( + this.#pickerInputContext.selection, + (selection) => (this.value = selection.join(',')), + '_observeSelection', + ); + this.observe( + this.#pickerInputContext.selectedItems, + (selectedItems) => (this._items = selectedItems), + '_observerItems', + ); + + this.observe(this.#pickerInputContext.statuses, (statuses) => (this._statuses = statuses), '_observerStatuses'); + } + + protected override getFormElement() { + return undefined; + } + + #openPicker() { + this.#pickerInputContext.openPicker(); + } + + #onRemove(unique: string) { + this.#pickerInputContext.requestRemoveItem(unique); + } + + override render() { + return html`${this.#renderItems()} ${this.#renderAddButton()}`; + } + + #renderAddButton() { + if (this.max > 0 && this.selection.length >= this.max) return nothing; + return html` + + `; + } + + #renderItems() { + if (!this._statuses) return; + return html` + + ${repeat( + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + return html` + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + `; + }, + )} + + `; + } + + static override styles = [ + css` + #btn-add { + width: 100%; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-property-editor-data-source': UmbInputPropertyEditorDataSourceElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/constants.ts new file mode 100644 index 0000000000..1d1e114de8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/constants.ts @@ -0,0 +1 @@ +export * from './data/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/constants.ts new file mode 100644 index 0000000000..6ea1a1a23e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/constants.ts @@ -0,0 +1,3 @@ +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_REPOSITORY_ALIAS = 'Umb.Repository.PropertyEditorDataSourceItem'; +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_ALIAS = 'Umb.Store.PropertyEditorDataSourceItem'; +export { UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_CONTEXT } from './item.store.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/index.ts new file mode 100644 index 0000000000..fb21e00f3e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/index.ts @@ -0,0 +1 @@ +export { UmbPropertyEditorDataSourceItemRepository } from './item.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.extension-registry.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.extension-registry.data-source.ts new file mode 100644 index 0000000000..f0cf06c1d8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.extension-registry.data-source.ts @@ -0,0 +1,31 @@ +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE } from '../../entity.js'; +import type { UmbPropertyEditorDataSourceItemModel } from './types.js'; +import type { UmbItemDataSource } from '@umbraco-cms/backoffice/repository'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +/** + * A server data source for Property Editor Data Source items + * @class UmbPropertyEditorDataSourceItemExtensionRegistryDataSource + * @implements {UmbItemDataSource} + */ +export class UmbPropertyEditorDataSourceItemExtensionRegistryDataSource + implements UmbItemDataSource +{ + async getItems(uniques: Array) { + if (!uniques) throw new Error('Uniques are missing'); + + const extensions = umbExtensionsRegistry.getByType('propertyEditorDataSource'); + + const items: Array = extensions + .filter((manifest) => uniques.includes(manifest.alias)) + .map((manifest) => ({ + entityType: UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE, + unique: manifest.alias, + name: manifest.meta.label ?? manifest.name, + icon: manifest.meta.icon ?? 'icon-database', + description: manifest.meta?.description ?? '', + })); + + return { data: items }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.repository.ts new file mode 100644 index 0000000000..3049951d30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.repository.ts @@ -0,0 +1,17 @@ +import { UmbPropertyEditorDataSourceItemExtensionRegistryDataSource } from './item.extension-registry.data-source.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_CONTEXT } from './item.store.context-token.js'; +import type { UmbPropertyEditorDataSourceItemModel } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbPropertyEditorDataSourceItemRepository extends UmbItemRepositoryBase { + constructor(host: UmbControllerHost) { + super( + host, + UmbPropertyEditorDataSourceItemExtensionRegistryDataSource, + UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_CONTEXT, + ); + } +} + +export { UmbPropertyEditorDataSourceItemRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.store.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.store.context-token.ts new file mode 100644 index 0000000000..3b78654005 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.store.context-token.ts @@ -0,0 +1,5 @@ +import type { UmbPropertyEditorDataSourceItemStore } from './item.store.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_CONTEXT = + new UmbContextToken('UmbPropertyEditorDataSourceItemStore'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.store.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.store.ts new file mode 100644 index 0000000000..350e454a06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/item.store.ts @@ -0,0 +1,23 @@ +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_CONTEXT } from './item.store.context-token.js'; +import type { UmbPropertyEditorDataSourceItemModel } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemStoreBase } from '@umbraco-cms/backoffice/store'; + +/** + * @class UmbPropertyEditorDataSourceItemStore + * @augments {UmbStoreBase} + * @description - Data Store for Property Editor Data Source items + */ + +export class UmbPropertyEditorDataSourceItemStore extends UmbItemStoreBase { + /** + * Creates an instance of UmbPropertyEditorDataSourceItemStore. + * @param {UmbControllerHost} host - The controller host for this controller to be appended to + * @memberof UmbPropertyEditorDataSourceItemStore + */ + constructor(host: UmbControllerHost) { + super(host, UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_CONTEXT.toString()); + } +} + +export { UmbPropertyEditorDataSourceItemStore as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/manifests.ts new file mode 100644 index 0000000000..0f615bef2c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/manifests.ts @@ -0,0 +1,19 @@ +import { + UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_REPOSITORY_ALIAS, + UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_ALIAS, +} from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_REPOSITORY_ALIAS, + name: 'Property Editor Data Source Item Repository', + api: () => import('./item.repository.js'), + }, + { + type: 'itemStore', + alias: UMB_PROPERTY_EDITOR_DATA_SOURCE_ITEM_STORE_ALIAS, + name: 'Property Editor Data Source Item Store', + api: () => import('./item.store.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/types.ts new file mode 100644 index 0000000000..806e86633a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/data/types.ts @@ -0,0 +1,5 @@ +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; +export interface UmbPropertyEditorDataSourceItemModel extends UmbItemModel { + description?: string; + name: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/manifests.ts new file mode 100644 index 0000000000..4007d5d37f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as dataManifests } from './data/manifests.js'; +import { manifests as refManifests } from './ref/manifests.js'; + +export const manifests = [...dataManifests, ...refManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/ref/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/ref/manifests.ts new file mode 100644 index 0000000000..7cfbd4bf16 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/ref/manifests.ts @@ -0,0 +1,11 @@ +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE } from '../../entity.js'; + +export const manifests: Array = [ + { + type: 'entityItemRef', + alias: 'Umb.EntityItemRef.PropertyEditorDataSource', + name: 'Property Editor Data Source Item Reference', + element: () => import('./property-editor-data-source-item-ref.element.js'), + forEntityTypes: [UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/ref/property-editor-data-source-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/ref/property-editor-data-source-item-ref.element.ts new file mode 100644 index 0000000000..a2874cbd5a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/ref/property-editor-data-source-item-ref.element.ts @@ -0,0 +1,53 @@ +import type { UmbPropertyEditorDataSourceItemModel } from '../types.js'; +import { css, customElement, html, ifDefined, nothing, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-property-editor-data-source-item-ref') +export class UmbPropertyEditorDataSourceItemRefElement extends UmbLitElement { + #item?: UmbPropertyEditorDataSourceItemModel | undefined; + + @property({ type: Object }) + public get item(): UmbPropertyEditorDataSourceItemModel | undefined { + return this.#item; + } + public set item(value: UmbPropertyEditorDataSourceItemModel | undefined) { + this.#item = value; + } + + @property({ type: Boolean }) + readonly = false; + + @property({ type: Boolean }) + standalone = false; + + override render() { + if (!this.item) return nothing; + + return html` + + + + + `; + } + + static override styles = [ + css` + umb-user-avatar { + font-size: var(--uui-size-4); + } + `, + ]; +} + +export { UmbPropertyEditorDataSourceItemRefElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-data-source-item-ref': UmbPropertyEditorDataSourceItemRefElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/types.ts new file mode 100644 index 0000000000..185b39805d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/item/types.ts @@ -0,0 +1 @@ +export type * from './data/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/manifests.ts new file mode 100644 index 0000000000..c76a1b7eff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/manifests.ts @@ -0,0 +1,5 @@ +import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as itemManifests } from './item/manifests.js'; +import { manifests as searchManifests } from './search/manifests.js'; + +export const manifests: Array = [...collectionManifests, ...itemManifests, ...searchManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/constants.ts new file mode 100644 index 0000000000..aef1a96e17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/constants.ts @@ -0,0 +1 @@ +export const UMB_PROPERTY_EDITOR_DATA_SOURCE_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.PropertyEditorDataSource'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/index.ts new file mode 100644 index 0000000000..4f07201dcf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/index.ts @@ -0,0 +1 @@ +export * from './constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/manifests.ts new file mode 100644 index 0000000000..4d324f2a22 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/manifests.ts @@ -0,0 +1,20 @@ +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE } from '../entity.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_SEARCH_PROVIDER_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'searchProvider', + alias: UMB_PROPERTY_EDITOR_DATA_SOURCE_SEARCH_PROVIDER_ALIAS, + name: 'Property Editor Data Source Search Provider', + api: () => import('./search-provider.js'), + weight: 600, + }, + + { + type: 'pickerSearchResultItem', + kind: 'default', + alias: 'Umb.PickerSearchResultItem.PropertyEditorDataSource', + name: 'Property Editor Data Source Picker Search Result Item', + forEntityTypes: [UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search-provider.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search-provider.ts new file mode 100644 index 0000000000..8df415d236 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search-provider.ts @@ -0,0 +1,21 @@ +import type { UmbPropertyEditorDataSourceItemModel } from '../item/types.js'; +import { UmbPropertyEditorDataSourceSearchRepository } from './search.repository.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbSearchProvider, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export class UmbPropertyEditorDataSourceSearchProvider + extends UmbControllerBase + implements UmbSearchProvider +{ + #repository = new UmbPropertyEditorDataSourceSearchRepository(this); + + async search(args: UmbSearchRequestArgs) { + return this.#repository.search(args); + } + + override destroy(): void { + this.#repository.destroy(); + } +} + +export { UmbPropertyEditorDataSourceSearchProvider as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search.extension-registry.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search.extension-registry.data-source.ts new file mode 100644 index 0000000000..b97cbccaa4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search.extension-registry.data-source.ts @@ -0,0 +1,53 @@ +import type { UmbPropertyEditorDataSourceItemModel } from '../item/types.js'; +import { UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE } from '../entity.js'; +import type { UmbPropertyEditorDataSourceSearchRequestArgs } from './types.js'; +import type { UmbSearchDataSource } from '@umbraco-cms/backoffice/search'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +/** + * A data source for the Property Editor Data Source search, that uses the Extension Registry to find available data sources. + * @class UmbPropertyEditorDataSourceSearchExtensionRegistryDataSource + * @augments {UmbControllerBase} + * @implements {UmbSearchDataSource} + */ +export class UmbPropertyEditorDataSourceSearchExtensionRegistryDataSource + extends UmbControllerBase + implements UmbSearchDataSource +{ + async search(args: UmbPropertyEditorDataSourceSearchRequestArgs) { + const extensions = umbExtensionsRegistry.getByType('propertyEditorDataSource'); + + const extensionsWithAllowedDataSourceTypes = extensions.filter((ext) => { + if (args.dataSourceTypes && args.dataSourceTypes.length > 0) { + return args.dataSourceTypes.includes(ext.dataSourceType); + } + return true; + }); + + const lowerCaseQuery = args.query.toLowerCase(); + + // Simple filter by name or alias + const filteredExtensions = extensionsWithAllowedDataSourceTypes.filter( + (item) => + item.meta.label.toLowerCase().includes(lowerCaseQuery) || + item.name.toLowerCase().includes(lowerCaseQuery) || + item.alias.toLowerCase().includes(lowerCaseQuery), + ); + + const items: UmbPropertyEditorDataSourceItemModel[] = filteredExtensions.map((extension) => ({ + entityType: UMB_PROPERTY_EDITOR_DATA_SOURCE_ENTITY_TYPE, + unique: extension.alias, + name: extension.meta.label, + icon: extension.meta?.icon ?? 'icon-database', + description: extension.meta?.description ?? '', + })); + + return { + data: { + items: items, + total: items.length, + }, + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search.repository.ts new file mode 100644 index 0000000000..9a8e94cdf7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/search.repository.ts @@ -0,0 +1,23 @@ +import type { UmbPropertyEditorDataSourceItemModel } from '../item/types.js'; +import { UmbPropertyEditorDataSourceSearchExtensionRegistryDataSource } from './search.extension-registry.data-source.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbSearchRepository, UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export class UmbPropertyEditorDataSourceSearchRepository + extends UmbControllerBase + implements UmbSearchRepository, UmbApi +{ + #dataSource: UmbPropertyEditorDataSourceSearchExtensionRegistryDataSource; + + constructor(host: UmbControllerHost) { + super(host); + + this.#dataSource = new UmbPropertyEditorDataSourceSearchExtensionRegistryDataSource(this); + } + + search(args: UmbSearchRequestArgs) { + return this.#dataSource.search(args); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/types.ts new file mode 100644 index 0000000000..2a7db547d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/search/types.ts @@ -0,0 +1,5 @@ +import type { UmbSearchRequestArgs } from '@umbraco-cms/backoffice/search'; + +export interface UmbPropertyEditorDataSourceSearchRequestArgs extends UmbSearchRequestArgs { + dataSourceTypes?: Array; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/types.ts new file mode 100644 index 0000000000..90c1bd6808 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor-data-source/types.ts @@ -0,0 +1 @@ +export type * from './extension/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts index 836b06bfd3..6b25c6cc0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts @@ -5,6 +5,7 @@ export interface UmbPropertyEditorUiElement extends HTMLElement { manifest?: ManifestPropertyEditorUi; name?: string; value?: unknown; + dataSourceAlias?: string; config?: UmbPropertyEditorConfigCollection; readonly?: boolean; mandatory?: boolean; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts index 708b590b56..31ff43ed7b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor.extension.ts @@ -25,6 +25,19 @@ export interface MetaPropertyEditorUi { propertyEditorSchemaAlias?: string; settings?: PropertyEditorSettings; supportsReadOnly?: boolean; + supportsDataSource?: { + /** + * Whether the property editor UI is enabled for use with data sources. + * @type {boolean} + */ + enabled: boolean; + /** + * A list of allowed property editor data source kinds that can be used with this property editor UI. + * If not specified, any data sources can be used. + * @example ["pickerCollection", "pickerTree"] + */ + forDataSourceTypes: string[]; + }; } // Model @@ -53,6 +66,7 @@ export interface PropertyEditorSettingsProperty { description?: string; alias: string; propertyEditorUiAlias: string; + propertyEditorDataSourceAlias?: string; config?: UmbPropertyEditorConfig; weight?: number; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts index 0fe89b19f7..424c5196cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts @@ -60,6 +60,9 @@ export class UmbPropertyContext extends UmbContextBase { #editorManifest = new UmbBasicState(undefined); public readonly editorManifest = this.#editorManifest.asObservable(); + #editorDataSourceAlias = new UmbStringState(undefined); + public readonly editorDataSourceAlias = this.#editorDataSourceAlias.asObservable(); + public readonly readOnlyState = new UmbReadOnlyStateManager(this); public readonly isReadOnly = this.readOnlyState.isReadOnly; @@ -95,6 +98,22 @@ export class UmbPropertyContext extends UmbContextBase { return this.#editorManifest.getValue(); } + /** + * Set the editor data source alias for this property. + * @param {string | undefined} dataSourceAlias The data source alias to set + */ + setEditorDataSourceAlias(dataSourceAlias: string | undefined) { + this.#editorDataSourceAlias.setValue(dataSourceAlias ?? undefined); + } + + /** + * Get the editor data source alias for this property. + * @returns {string | undefined} The editor data source alias for this property. + */ + getEditorDataSourceAlias(): string | undefined { + return this.#editorDataSourceAlias.getValue(); + } + // property variant ID: #variantId = new UmbClassState(undefined); public readonly variantId = this.#variantId.asObservable(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts index c46ecc5059..4684e09c9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.element.ts @@ -99,6 +99,18 @@ export class UmbPropertyElement extends UmbLitElement { } private _propertyEditorUiAlias?: string; + @property({ type: String, attribute: 'property-editor-data-source-alias' }) + public set propertyEditorDataSourceAlias(value: string | undefined) { + this.#propertyContext.setEditorDataSourceAlias(value); + + if (this._element) { + this._element.dataSourceAlias = value; + } + } + public get propertyEditorDataSourceAlias(): string | undefined { + return this.#propertyContext.getEditorDataSourceAlias(); + } + /** * Config. Configuration to pass to the Property Editor UI. This is also the configuration data stored on the Data Type. * @public @@ -344,6 +356,7 @@ export class UmbPropertyElement extends UmbLitElement { this._element.manifest = manifest; this._element.mandatory = this._mandatory; this._element.name = this._label; + this._element.dataSourceAlias = this.#propertyContext.getEditorDataSourceAlias(); // No need for a controller alias, as the clean is handled via the observer prop: this.#valueObserver = this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/read/read-detail-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/read/read-detail-repository.interface.ts index a19f530b19..a20af58788 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/read/read-detail-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/read/read-detail-repository.interface.ts @@ -4,5 +4,5 @@ import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; export interface UmbReadDetailRepository extends UmbApi { requestByUnique(unique: string): Promise>; - byUnique(unique: string): Promise>; + byUnique?(unique: string): Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-repository.interface.ts index 71e25909a2..58d8590939 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/item/item-repository.interface.ts @@ -4,5 +4,5 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; export interface UmbItemRepository extends UmbApi { requestItems: (uniques: string[]) => Promise>; - items: (uniques: string[]) => Promise> | undefined>; + items?: (uniques: string[]) => Promise> | undefined>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index f2352d1407..445e37955c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -15,6 +15,8 @@ export class UmbRepositoryItemsManager exte repository?: UmbItemRepository; #init: Promise; + #initResolve!: (value: unknown) => void; + #currentRequest?: Promise; #eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; @@ -39,18 +41,16 @@ export class UmbRepositoryItemsManager exte * @param {string} repositoryAlias - The alias of the repository to use. * @memberof UmbRepositoryItemsManager */ - constructor(host: UmbControllerHost, repositoryAlias: string) { + constructor(host: UmbControllerHost, repositoryAlias?: string) { super(host); - this.#init = new UmbExtensionApiInitializer>>( - this, - umbExtensionsRegistry, - repositoryAlias, - [this], - (permitted, repository) => { - this.repository = permitted ? repository.api : undefined; - }, - ).asPromise(); + this.#init = new Promise((resolve) => { + this.#initResolve = resolve; + }); + + if (repositoryAlias) { + this.#initItemRepository(repositoryAlias); + } this.observe( this.uniques, @@ -90,6 +90,25 @@ export class UmbRepositoryItemsManager exte }); } + /** + * Sets the item repository to use for this manager. + * @param {(UmbItemRepository | undefined)} itemRepository - The item repository to set. + * @memberof UmbRepositoryItemsManager + */ + setItemRepository(itemRepository: UmbItemRepository | undefined) { + this.repository = itemRepository; + this.#initResolve(undefined); + } + + /** + * Gets the item repository used by this manager. + * @returns {(UmbItemRepository | undefined)} The item repository. + * @memberof UmbRepositoryItemsManager + */ + getItemRepository(): UmbItemRepository | undefined { + return this.repository; + } + getUniques(): Array { return this.#uniques.getValue(); } @@ -196,6 +215,8 @@ export class UmbRepositoryItemsManager exte }, ObserveRepositoryAlias, ); + } else if (data) { + this.#items.setValue(data); } } @@ -251,6 +272,19 @@ export class UmbRepositoryItemsManager exte this.#reloadItem(item.unique); }; + async #initItemRepository(itemRepositoryAlias: string) { + new UmbExtensionApiInitializer>>( + this, + umbExtensionsRegistry, + itemRepositoryAlias, + [this], + (permitted, repository) => { + this.repository = permitted ? repository.api : undefined; + this.#initResolve(undefined); + }, + ); + } + override destroy(): void { this.#eventContext?.removeEventListener( UmbEntityUpdatedEvent.TYPE, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/types.ts index 83bafd3c7d..8643aa5894 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/types.ts @@ -31,7 +31,7 @@ export interface UmbRepositoryItemsStatus { * @template T$ - The type of items returned by the asObservable method, defaults to T. You should only use this if you want to return a different type from the asObservable method. */ export interface UmbRepositoryResponseWithAsObservable extends UmbRepositoryResponse { - asObservable: () => Observable | undefined; + asObservable?: () => Observable | undefined; } export type * from './data-mapper/mapping/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/dashboard-examine-management.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/dashboard-examine-management.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/dashboard-examine-management.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/dashboard-examine-management.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-settings/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-settings/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-settings/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-settings/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-settings/examine-fields-settings-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-viewer/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-viewer/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-viewer/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-viewer/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/fields-viewer/examine-fields-viewer-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/modal/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/modal/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/views/section-view-examine-indexers.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/views/section-view-examine-indexers.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/views/section-view-examine-indexers.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/views/section-view-examine-indexers.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/views/section-view-examine-overview.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/views/section-view-examine-overview.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/views/section-view-examine-overview.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/views/section-view-examine-overview.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/views/section-view-examine-searchers.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/views/section-view-examine-searchers.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/examine-management-dashboard/views/section-view-examine-searchers.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/examine-management-dashboard/views/section-view-examine-searchers.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/extensions/search-provider.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/extensions/search-provider.extension.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/extensions/search-provider.extension.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/extensions/search-provider.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/extensions/search-result-item.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/extensions/search-result-item.extension.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/extensions/search-result-item.extension.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/extensions/search-result-item.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/extensions/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/extensions/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/extensions/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/global-search-base.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search-base.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/global-search-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/global-search.extension.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/global-search/global-search.extension.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/global-search.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/global-search/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/global-search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/global-search/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/global-search/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/manifests.ts similarity index 86% rename from src/Umbraco.Web.UI.Client/src/packages/search/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/manifests.ts index bcdee40ae3..c5fd2f6a8b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/search/manifests.ts @@ -1,6 +1,5 @@ import { manifests as examineManifests } from './examine-management-dashboard/manifests.js'; import { UMB_SECTION_ALIAS_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; -import { UMB_SETTINGS_SECTION_ALIAS } from '@umbraco-cms/backoffice/settings'; export const manifests: Array = [ { @@ -34,7 +33,10 @@ export const manifests: Array = [ conditions: [ { alias: UMB_SECTION_ALIAS_CONDITION_ALIAS, - match: UMB_SETTINGS_SECTION_ALIAS, + /* TODO: kept as a magic string to avoid illegal import outside of core + Move dashboard implementation out of core + */ + match: 'Umb.Section.Settings', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/search-data-source.interface.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/search-data-source.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/search-data-source.interface.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/search-modal/search-modal.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/search-modal/search-modal.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/search-modal/search-modal.token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/search-modal/search-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/search-modal/search-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/search-repository.interface.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/search-repository.interface.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/search-repository.interface.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/search-result/search-result-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/search-result/search-result-item.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/search-result/search-result-item.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/search-result/search-result-item.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/types.ts similarity index 85% rename from src/Umbraco.Web.UI.Client/src/packages/search/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/types.ts index 664ca34f3a..7792da53cf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/search/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/search/types.ts @@ -1,4 +1,5 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import type { FieldPresentationModel, SearchResultResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbPagedModel, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository'; @@ -11,14 +12,9 @@ export type * from './global-search/types.js'; export type UmbSearchResultModel = SearchResultResponseModel; -// TODO: lower requirement for search provider item type -export type UmbSearchResultItemModel = { - entityType: string; - icon?: string | null; - name: string; - unique: string; - href: string; -}; +export interface UmbSearchResultItemModel extends UmbItemModel { + href?: string; +} export type UmbSearchRequestArgs = { query: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/umb-search-header-app.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/search/umb-search-header-app.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/search/umb-search-header-app.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/search/umb-search-header-app.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.context.ts index adc42d14d7..f74644a0ab 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/components/input-section/input-section.context.ts @@ -4,7 +4,7 @@ import { UMB_SECTION_ITEM_REPOSITORY_ALIAS } from '../../constants.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbSectionPickerInputContext extends UmbPickerInputContext { +export class UmbSectionPickerInputContext extends UmbPickerInputContext { constructor(host: UmbControllerHost) { super(host, UMB_SECTION_ITEM_REPOSITORY_ALIAS, UMB_SECTION_PICKER_MODAL); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/section-item.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/section-item.repository.ts index 97772fc132..c76caf3d3d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/section-item.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/section-item.repository.ts @@ -51,6 +51,8 @@ const sectionItemsByUniquesObservable = (uniques: Array) => const itemMapper = (manifest: ManifestSection): UmbSectionItemModel => { return { ...manifest, + // TODO: introduce const + entityType: 'section', unique: manifest.alias, }; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/types.ts index 358477d5a3..a4162b8c36 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/repository/item/types.ts @@ -1,5 +1,7 @@ import type { ManifestSection } from '../../extensions/index.js'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; -export interface UmbSectionItemModel extends ManifestSection { - unique: string; +// TODO: remove extension of ManifestSection +export interface UmbSectionItemModel extends UmbItemModel, ManifestSection { + name: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts index f2aa523e7e..d110f1c5ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts @@ -62,7 +62,7 @@ export interface UmbTreeRepository< * @memberof UmbTreeRepository * @deprecated Use `requestTreeRootItems` instead. It will be removed in Umbraco 18. */ - rootTreeItems: () => Promise>; + rootTreeItems?: () => Promise>; /** * Returns an observable of the children of the given parent item. @@ -70,5 +70,5 @@ export interface UmbTreeRepository< * @memberof UmbTreeRepository * @deprecated Use `requestTreeItemsOf` instead. It will be removed in Umbraco 18. */ - treeItemsOf: (parentUnique: string | null) => Promise>; + treeItemsOf?: (parentUnique: string | null) => Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts index bd85ac9068..d1714bdb2b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item.element.ts @@ -1,4 +1,5 @@ import type { ManifestTreeItem } from '../extensions/types.js'; +import UmbDefaultTreeItemContext from './tree-item-default/tree-item-default.context.js'; import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbExtensionElementAndApiSlotElementBase, @@ -18,6 +19,15 @@ export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase } #entityType?: string; + @property({ type: Object, attribute: false }) + override set props(newVal: Record | undefined) { + super.props = newVal; + this.#assignProps(); + } + override get props() { + return super.props; + } + #observeEntityType() { if (!this.#entityType) return; @@ -26,6 +36,8 @@ export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase return manifest.forEntityTypes.includes(this.#entityType); }; + // Check if we can find a matching tree item for the current entity type. + // If we can, we will use that one, if not we will render a fallback tree item. this.observe( // TODO: what should we do if there are multiple tree items for an entity type? // This method gets all extensions based on a type, then filters them based on the entity type. and then we get the alias of the first one [NL] @@ -35,11 +47,27 @@ export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase ), (alias) => { this.alias = alias; + + // If we don't find any registered tree items for this specific entity type, we will render a fallback tree item. + // This is on purpose not done with the extension initializer since we don't want to spin up a real extension unless we have to. + if (!alias) { + this.#renderFallbackItem(); + } }, 'umbObserveAlias', ); } + #renderFallbackItem() { + // TODO: make creating of elements with apis a shared function. + const element = document.createElement('umb-default-tree-item'); + const api = new UmbDefaultTreeItemContext(element); + element.api = api; + this._element = element; + this.#assignProps(); + this.requestUpdate('_element'); + } + getExtensionType() { return 'treeItem'; } @@ -47,6 +75,18 @@ export class UmbTreeItemElement extends UmbExtensionElementAndApiSlotElementBase getDefaultElementName() { return 'umb-default-tree-item'; } + + #assignProps() { + if (!this._element || !this.props) return; + + Object.keys(this.props).forEach((key) => { + (this._element as any)[key] = this.props![key]; + }); + } + + override getDefaultApiConstructor() { + return UmbDefaultTreeItemContext; + } } declare global { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/index.ts index 7cbd303dac..96c06454e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/index.ts @@ -1,3 +1,4 @@ export { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js'; -export type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker-modal.token.js'; export { UMB_TREE_PICKER_MODAL } from './tree-picker-modal.token.js'; + +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts index fb71dd349d..da5e0e8fc3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts @@ -1,7 +1,7 @@ import { UmbTreeItemPickerContext } from '../tree-item-picker/index.js'; import type { UmbTreeElement } from '../tree.element.js'; import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '../types.js'; -import type { UmbTreePickerModalData, UmbTreePickerModalValue } from './tree-picker-modal.token.js'; +import type { UmbTreePickerModalData, UmbTreePickerModalValue } from './types.js'; import { customElement, html, ifDefined, nothing, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts index e6acaf9979..36463e0870 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.token.ts @@ -1,33 +1,6 @@ -import type { UmbTreeItemModel, UmbTreeStartNode } from '../types.js'; import { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js'; -import type { UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; -import type { UmbWorkspaceModalData } from '@umbraco-cms/backoffice/workspace'; +import type { UmbTreePickerModalData, UmbTreePickerModalValue } from './types.js'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -import type { UmbPathPattern, UmbPathPatternParamsType } from '@umbraco-cms/backoffice/router'; - -export interface UmbTreePickerModalCreateActionData { - label: string; - modalData: UmbWorkspaceModalData; - modalToken?: UmbModalToken; - extendWithPathPattern: UmbPathPattern; - extendWithPathParams: PathPatternParamsType; -} - -export interface UmbTreePickerModalData< - TreeItemType = UmbTreeItemModel, - PathPatternParamsType extends UmbPathPatternParamsType = UmbPathPatternParamsType, -> extends UmbPickerModalData { - hideTreeRoot?: boolean; - expandTreeRoot?: boolean; - treeAlias?: string; - // Consider if it makes sense to move this into the UmbPickerModalData interface, but for now this is a TreePicker feature. [NL] - createAction?: UmbTreePickerModalCreateActionData; - startNode?: UmbTreeStartNode; - foldersOnly?: boolean; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface UmbTreePickerModalValue extends UmbPickerModalValue {} export const UMB_TREE_PICKER_MODAL = new UmbModalToken( UMB_TREE_PICKER_MODAL_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/types.ts new file mode 100644 index 0000000000..3703665f6b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/types.ts @@ -0,0 +1,28 @@ +import type { UmbTreeItemModel, UmbTreeStartNode } from '../types.js'; +import type { UmbPathPattern, UmbPathPatternParamsType } from '@umbraco-cms/backoffice/router'; +import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; +import type { UmbWorkspaceModalData } from '@umbraco-cms/backoffice/workspace'; + +export interface UmbTreePickerModalCreateActionData { + label: string; + modalData: UmbWorkspaceModalData; + modalToken?: UmbModalToken; + extendWithPathPattern: UmbPathPattern; + extendWithPathParams: PathPatternParamsType; +} + +export interface UmbTreePickerModalData< + TreeItemType = UmbTreeItemModel, + PathPatternParamsType extends UmbPathPatternParamsType = UmbPathPatternParamsType, +> extends UmbPickerModalData { + hideTreeRoot?: boolean; + expandTreeRoot?: boolean; + treeAlias?: string; + // TODO: create action should be replaces by entity actions in the pickers. Then we also open up for creating folders, choosing where to place items etc. [MR] + createAction?: UmbTreePickerModalCreateActionData; + startNode?: UmbTreeStartNode; + foldersOnly?: boolean; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbTreePickerModalValue extends UmbPickerModalValue {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts new file mode 100644 index 0000000000..b4f03f1155 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/index.ts @@ -0,0 +1,12 @@ +import type { UmbConfigCollectionModel } from './types.js'; + +/** + * Get a value from a config collection by its alias. + * @param {UmbConfigCollectionModel | undefined} config - The config collection to get the value from. + * @param {string} alias - The alias of the value to get. + * @returns {T | undefined} The value with the specified alias, or undefined if not found or if the config is undefined. + */ +export function getConfigValue(config: UmbConfigCollectionModel | undefined, alias: string): T | undefined { + const entry = config?.find((entry) => entry.alias === alias); + return entry?.value as T | undefined; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/types.ts new file mode 100644 index 0000000000..736c411346 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/config-collection/types.ts @@ -0,0 +1,6 @@ +export interface UmbConfigCollectionEntryModel { + alias: string; + value: unknown; +} + +export type UmbConfigCollectionModel = Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index 33deb40984..12d0240363 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -1,5 +1,6 @@ export * from './array/index.js'; export * from './bytes/bytes.function.js'; +export * from './config-collection/index.js'; export * from './date/index.js'; export * from './debounce/debounce.function.js'; export * from './deprecation/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/type/index.ts index 613d220897..8e33fb6a58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/type/index.ts @@ -1,3 +1,4 @@ export type * from './deep-partial-object.type.js'; export type * from './diff.type.js'; export type * from './partial-some.type.js'; +export type * from '../config-collection/types.js'; 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 b77da682d3..5ba008ff34 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 @@ -46,8 +46,10 @@ export default defineConfig({ 'notification/index': './notification/index.ts', 'object-type/index': './object-type/index.ts', 'picker-input/index': './picker-input/index.ts', + 'picker-data-source/index': './picker-data-source/index.ts', 'picker/index': './picker/index.ts', 'property-action/index': './property-action/index.ts', + 'property-editor-data-source/index': './property-editor-data-source/index.ts', 'property-editor/index': './property-editor/index.ts', 'property/index': './property/index.ts', 'recycle-bin/index': './recycle-bin/index.ts', @@ -57,6 +59,7 @@ export default defineConfig({ 'section/index': './section/index.ts', 'server-file-system/index': './server-file-system/index.ts', 'server/index': './server/index.ts', + 'search/index': './search/index.ts', 'shortcut/index': './shortcut/index.ts', 'sorter/index': './sorter/index.ts', 'store/index': './store/index.ts', diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts index 25abcce916..4ee552be0c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/property-editor-config/property-editor-config.element.ts @@ -51,6 +51,7 @@ export class UmbPropertyEditorConfigElement extends UmbLitElement { description=${ifDefined(property.description)} alias=${property.alias} property-editor-ui-alias=${property.propertyEditorUiAlias} + property-editor-data-source-alias=${ifDefined(property.propertyEditorDataSourceAlias)} .config=${property.config}>`, ) : html` { if (dataType) { this.name = dataType.name ?? ''; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts index a79c9437c5..9b6d4ae062 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/detail/server-data-source/data-type-detail.server.data-source.ts @@ -20,6 +20,7 @@ export class UmbDataTypeServerDataSource implements UmbDetailDataSource { #detailRequestManager = new UmbManagementApiDataTypeDetailDataRequestManager(this); + #editorDataSourceAlias = 'umbEditorDataSource'; /** * Creates a new Data Type scaffold @@ -69,6 +70,17 @@ export class UmbDataTypeServerDataSource if (!model.editorAlias) throw new Error('Property Editor Alias is missing'); if (!model.editorUiAlias) throw new Error('Property Editor UI Alias is missing'); + const values = [...model.values]; + + if (model.editorDataSourceAlias) { + const editorDataSourceValue = { + alias: this.#editorDataSourceAlias, + value: model.editorDataSourceAlias, + }; + + values.unshift(editorDataSourceValue); + } + // TODO: make data mapper to prevent errors const body: CreateDataTypeRequestModel = { id: model.unique, @@ -76,7 +88,7 @@ export class UmbDataTypeServerDataSource name: model.name, editorAlias: model.editorAlias, editorUiAlias: model.editorUiAlias, - values: model.values, + values, }; const { data, error } = await this.#detailRequestManager.create(body); @@ -96,12 +108,23 @@ export class UmbDataTypeServerDataSource if (!model.editorAlias) throw new Error('Property Editor Alias is missing'); if (!model.editorUiAlias) throw new Error('Property Editor UI Alias is missing'); + const values = [...model.values]; + + if (model.editorDataSourceAlias) { + const editorDataSourceValue = { + alias: this.#editorDataSourceAlias, + value: model.editorDataSourceAlias, + }; + + values.unshift(editorDataSourceValue); + } + // TODO: make data mapper to prevent errors const body: UpdateDataTypeRequestModel = { name: model.name, editorAlias: model.editorAlias, editorUiAlias: model.editorUiAlias, - values: model.values, + values, }; const { data, error } = await this.#detailRequestManager.update(model.unique, body); @@ -122,13 +145,26 @@ export class UmbDataTypeServerDataSource // TODO: change this to a mapper extension when the endpoints returns a $type for DataTypeResponseModel #mapServerResponseModelToEntityDetailModel(data: DataTypeResponseModel): UmbDataTypeDetailModel { + let values = data.values as Array; + const index = values?.findIndex((x) => x.alias === this.#editorDataSourceAlias); + + let editorDataSourceAlias; + + /* Remove the editorDataSourceAlias from the values collection + to prevent it from being treated as a regular config value. */ + if (index !== -1) { + editorDataSourceAlias = values?.[index].value as string | null; + values = values?.filter((value) => value.alias !== this.#editorDataSourceAlias) ?? []; + } + return { entityType: UMB_DATA_TYPE_ENTITY_TYPE, unique: data.id, name: data.name, editorAlias: data.editorAlias, editorUiAlias: data.editorUiAlias || null, - values: data.values as Array, + editorDataSourceAlias: editorDataSourceAlias, + values, }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/types.ts index 38607afc84..571da307cd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/types.ts @@ -7,6 +7,7 @@ export interface UmbDataTypeDetailModel { name: string; editorAlias: string | undefined; editorUiAlias: string | null; + editorDataSourceAlias?: string | null; values: Array; } 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 e9176832de..19d507f51e 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 @@ -20,6 +20,7 @@ import type { PropertyEditorSettingsProperty, } from '@umbraco-cms/backoffice/property-editor'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorDataSource } from '@umbraco-cms/backoffice/property-editor-data-source'; type EntityType = UmbDataTypeDetailModel; @@ -46,6 +47,9 @@ export class UmbDataTypeWorkspaceContext { readonly propertyEditorUiAlias = this._data.createObservablePartOfCurrent((data) => data?.editorUiAlias); readonly propertyEditorSchemaAlias = this._data.createObservablePartOfCurrent((data) => data?.editorAlias); + readonly propertyEditorDataSourceAlias = this._data.createObservablePartOfCurrent( + (data) => data?.editorDataSourceAlias, + ); readonly values = this._data.createObservablePartOfCurrent((data) => data?.values); async getValues() { @@ -59,9 +63,11 @@ export class UmbDataTypeWorkspaceContext #propertyEditorSchemaSettingsDefaultData: Array = []; #propertyEditorUISettingsDefaultData: Array = []; + #propertyEditorDataSourceSettingsDefaultData: Array = []; #propertyEditorSchemaSettingsProperties: Array = []; #propertyEditorUISettingsProperties: Array = []; + #propertyEditorDataSourceSettingsProperties: Array = []; #propertyEditorSchemaConfigDefaultUIAlias: string | null = null; @@ -82,6 +88,7 @@ export class UmbDataTypeWorkspaceContext this.#observePropertyEditorSchemaAlias(); this.#observePropertyEditorUIAlias(); + this.#observePropertyEditorDataSourceAlias(); this.routes.setRoutes([ { @@ -116,6 +123,8 @@ export class UmbDataTypeWorkspaceContext this.#propertyEditorUISettingsProperties = []; this.#propertyEditorSchemaSettingsDefaultData = []; this.#propertyEditorUISettingsDefaultData = []; + this.#propertyEditorDataSourceSettingsProperties = []; + this.#propertyEditorDataSourceSettingsDefaultData = []; this.#settingsDefaultData = undefined; this.#mergeConfigProperties(); } @@ -151,6 +160,18 @@ export class UmbDataTypeWorkspaceContext ); } + #observePropertyEditorDataSourceAlias() { + return this.observe( + this.propertyEditorDataSourceAlias, + (propertyEditorDataSourceAlias) => { + this.#propertyEditorDataSourceSettingsProperties = []; + this.#propertyEditorDataSourceSettingsDefaultData = []; + this.#observePropertyEditorDataSourceManifest(propertyEditorDataSourceAlias); + }, + 'dataSourceAlias', + ); + } + #observePropertyEditorSchemaManifest(propertyEditorSchemaAlias?: string) { if (!propertyEditorSchemaAlias) { this.removeUmbControllerByAlias('schema'); @@ -205,12 +226,38 @@ export class UmbDataTypeWorkspaceContext ); } + #observePropertyEditorDataSourceManifest(propertyEditorDataSourceAlias: string | null | undefined) { + if (!propertyEditorDataSourceAlias) { + this.removeUmbControllerByAlias('dataSource'); + this.#mergeConfigProperties(); + return; + } + this.observe( + umbExtensionsRegistry.byAlias(propertyEditorDataSourceAlias), + (manifest) => { + // Maps properties to have a weight, so they can be sorted, notice data source properties have a +2000 weight compared to schema properties. + this.#propertyEditorDataSourceSettingsProperties = (manifest?.meta.settings?.properties ?? []).map((x, i) => ({ + ...x, + weight: x.weight ?? 2000 + i, + })); + this.#propertyEditorDataSourceSettingsDefaultData = manifest?.meta.settings?.defaultData || []; + this.#mergeConfigProperties(); + }, + 'dataSource', + ); + } + #mergeConfigProperties() { - if (this.#propertyEditorSchemaSettingsProperties && this.#propertyEditorUISettingsProperties) { - // Reset the value to this array, and then afterwards append: - this.#properties.setValue(this.#propertyEditorSchemaSettingsProperties); - // Append the UI settings properties to the schema properties, so they can override the schema properties: - this.#properties.append(this.#propertyEditorUISettingsProperties); + const settings = [ + this.#propertyEditorSchemaSettingsProperties, + this.#propertyEditorUISettingsProperties, + this.#propertyEditorDataSourceSettingsProperties, + ].filter((x) => Array.isArray(x) && x.length > 0); + + const mergedSettings = settings.flat(); + + if (mergedSettings) { + this.#properties.setValue(mergedSettings); // If new or if the alias was changed then set default values. This 'complexity' to prevent setting default data when initialized [NL] const previousPropertyEditorUIAlias = this.#lastPropertyEditorUIAlias; @@ -225,7 +272,12 @@ export class UmbDataTypeWorkspaceContext } #transferConfigDefaultData() { - if (!this.#propertyEditorSchemaSettingsDefaultData || !this.#propertyEditorUISettingsDefaultData) return; + if ( + !this.#propertyEditorSchemaSettingsDefaultData || + !this.#propertyEditorUISettingsDefaultData || + !this.#propertyEditorDataSourceSettingsDefaultData + ) + return; const data = this._data.getCurrent(); if (!data) return; @@ -236,6 +288,7 @@ export class UmbDataTypeWorkspaceContext this.#settingsDefaultData = [ ...this.#propertyEditorSchemaSettingsDefaultData, ...this.#propertyEditorUISettingsDefaultData, + ...this.#propertyEditorDataSourceSettingsDefaultData, ] satisfies Array; const values: Array = []; @@ -285,6 +338,14 @@ export class UmbDataTypeWorkspaceContext this._data.updateCurrent({ editorUiAlias: alias }); } + getPropertyEditorDataSourceAlias() { + return this._data.getCurrent()?.editorDataSourceAlias; + } + + setPropertyEditorDataSourceAlias(alias?: string) { + this._data.updateCurrent({ editorDataSourceAlias: alias }); + } + /** * @function propertyValueByAlias * @param {string} propertyAlias diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts index c49b12a2ca..e8a6f2767a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts @@ -4,6 +4,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS } from '@umbraco-cms/backoffice/property-editor'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; @customElement('umb-data-type-details-workspace-view') @@ -20,6 +21,15 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im @state() private _propertyEditorSchemaAlias?: string; + @state() + private _propertyEditorDataSourceAlias?: string | null = null; + + @state() + private _supportsDataSource = false; + + @state() + private _supportedDataSourceTypes: Array = []; + #workspaceContext?: typeof UMB_DATA_TYPE_WORKSPACE_CONTEXT.TYPE; constructor() { @@ -38,6 +48,7 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im this.observe(this.#workspaceContext.propertyEditorUiAlias, (value) => { this._propertyEditorUiAlias = value ?? undefined; + this.#observePropertyEditorUIManifest(); }); this.observe(this.#workspaceContext.propertyEditorSchemaAlias, (value) => { @@ -51,6 +62,24 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im this.observe(this.#workspaceContext.propertyEditorUiIcon, (value) => { this._propertyEditorUiIcon = value ?? undefined; }); + + this.observe(this.#workspaceContext.propertyEditorDataSourceAlias, (value) => { + this._propertyEditorDataSourceAlias = value; + }); + } + + #observePropertyEditorUIManifest() { + if (!this._propertyEditorUiAlias) return; + + this.observe(umbExtensionsRegistry.byTypeAndAlias('propertyEditorUi', this._propertyEditorUiAlias), (manifest) => { + this._supportsDataSource = manifest?.meta?.supportsDataSource?.enabled ?? false; + this._supportedDataSourceTypes = manifest?.meta?.supportsDataSource?.forDataSourceTypes ?? []; + }); + } + + #onDataSourceChange(event: CustomEvent) { + const value = (event.target as HTMLInputElement).value; + this.#workspaceContext?.setPropertyEditorDataSourceAlias(value || undefined); } override render() { @@ -70,24 +99,40 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im ${umbBindToValidation(this, '$.editorUiAlias', this._propertyEditorUiAlias)}> + ${this.#renderDataSourceInput()} ${this.#renderSettings()} `; } + #renderDataSourceInput() { + if (!this._supportsDataSource) return nothing; + + return html` + + + + `; + } + #renderSettings() { if ( !this._propertyEditorUiAlias || !this._propertyEditorUiName || !this._propertyEditorSchemaAlias || this._propertyEditorUiAlias === UMB_MISSING_PROPERTY_EDITOR_UI_ALIAS - ) + ) { return nothing; - return html` - - - - `; + } + + return html` + + `; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/info/workspace-view-data-type-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/info/workspace-view-data-type-info.element.ts index fb9188218b..c48593fb0c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/info/workspace-view-data-type-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/info/workspace-view-data-type-info.element.ts @@ -1,6 +1,6 @@ import { UMB_DATA_TYPE_WORKSPACE_CONTEXT } from '../../data-type-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; @@ -15,6 +15,9 @@ export class UmbWorkspaceViewDataTypeInfoElement extends UmbLitElement implement @state() private _uiAlias?: string | null; + @state() + private _dataSourceAlias?: string | null; + private _workspaceContext?: typeof UMB_DATA_TYPE_WORKSPACE_CONTEXT.TYPE; constructor() { @@ -40,6 +43,10 @@ export class UmbWorkspaceViewDataTypeInfoElement extends UmbLitElement implement this.observe(this._workspaceContext.propertyEditorUiAlias, (editorUiAlias) => { this._uiAlias = editorUiAlias; }); + + this.observe(this._workspaceContext.propertyEditorDataSourceAlias, (dataSourceAlias) => { + this._dataSourceAlias = dataSourceAlias; + }); } override render() { @@ -66,10 +73,22 @@ export class UmbWorkspaceViewDataTypeInfoElement extends UmbLitElement implement Property Editor UI Alias ${this._uiAlias} + ${this.#renderDataSourceInfo()} `; } + #renderDataSourceInfo() { + if (!this._dataSourceAlias) return nothing; + + return html` +
+ Property Editor Data Source Alias + ${this._dataSourceAlias} +
+ `; + } + static override styles = [ UmbTextStyles, css` diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts index 317a4ffda7..47e72a7b23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts @@ -13,6 +13,7 @@ export * from './publishing/index.js'; export * from './recycle-bin/index.js'; export * from './reference/index.js'; export * from './repository/index.js'; +export * from './search/index.js'; export * from './tree/index.js'; export * from './url/index.js'; export * from './user-permissions/index.js'; 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 index 4f07201dcf..ac0ad7d244 100644 --- 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 @@ -1 +1,2 @@ export * from './constants.js'; +export * from './document-search.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts index 6ac1773e53..94763610fe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/types.ts @@ -17,6 +17,7 @@ export type * from './modals/types.js'; export type * from './preview/types.js'; export type * from './publishing/types.js'; export type * from './recycle-bin/types.js'; +export type * from './search/types.js'; export type * from './repository/types.js'; export type * from './tree/types.js'; export type * from './url/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.context.ts index caf52fdb90..d0db74ad8d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/components/input-language/input-language.context.ts @@ -4,7 +4,11 @@ import { UMB_LANGUAGE_PICKER_MODAL } from '../../modals/language-picker/constant import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; -export class UmbLanguagePickerInputContext extends UmbPickerInputContext { +export class UmbLanguagePickerInputContext extends UmbPickerInputContext< + UmbLanguageItemModel, + // TODO: add UmbLanguageCollectionItemModel when it's created + UmbLanguageItemModel +> { constructor(host: UmbControllerHost) { super(host, UMB_LANGUAGE_ITEM_REPOSITORY_ALIAS, UMB_LANGUAGE_PICKER_MODAL); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 7642a23e83..8974dc6ebb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -37,7 +37,7 @@ const root: UmbMediaPathModel = { name: 'Media', unique: null, entityType: UMB_M // TODO: investigate how we can reuse the picker-search-field element, picker context etc. @customElement('umb-media-picker-modal') export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement< - UmbMediaItemModel, + UmbMediaTreeItemModel, UmbMediaPickerModalData, UmbMediaPickerModalValue > { @@ -110,6 +110,9 @@ export class UmbMediaPickerModalElement extends UmbPickerModalBaseElement< override async connectedCallback(): Promise { super.connectedCallback(); if (this.data?.pickableFilter) { + // TODO: investigate why we need the ts-ignore here + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this._selectableFilter = this.data?.pickableFilter; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts index aebece4e35..91ba37a784 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -1,8 +1,8 @@ -import type { UmbMediaItemModel } from '../../repository/types.js'; +import type { UmbMediaTreeItemModel } from '../../tree/types.js'; import type { UmbTreePickerModalData } from '@umbraco-cms/backoffice/tree'; import { UmbModalToken, type UmbPickerModalValue } from '@umbraco-cms/backoffice/modal'; -export type UmbMediaPickerModalData = UmbTreePickerModalData; +export type UmbMediaPickerModalData = UmbTreePickerModalData; export type UmbMediaPickerModalValue = UmbPickerModalValue; export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/index.ts index 8442641321..b7ce4ba6d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/search/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/search/index.ts @@ -1 +1,2 @@ export * from './media.search-provider.js'; +export * from './media-search.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts index 4f56828544..465a7a4eff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts @@ -4,7 +4,11 @@ import { UMB_MEMBER_GROUP_PICKER_MODAL } from '../member-group-picker-modal/memb import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMemberGroupPickerInputContext extends UmbPickerInputContext { +export class UmbMemberGroupPickerInputContext extends UmbPickerInputContext< + UmbMemberGroupItemModel, + // TODO: Change to UmbMemberGroupCollectionItemModel when it exists + UmbMemberGroupItemModel +> { constructor(host: UmbControllerHostElement) { super(host, UMB_MEMBER_GROUP_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_GROUP_PICKER_MODAL); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/types.ts index 76290819a6..10f3d84206 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/item/types.ts @@ -1,7 +1,5 @@ -import type { UmbMemberGroupEntityType } from '../../entity.js'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; -export interface UmbMemberGroupItemModel { - entityType: UmbMemberGroupEntityType; - unique: string; +export interface UmbMemberGroupItemModel extends UmbItemModel { name: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/constants.ts new file mode 100644 index 0000000000..589e5fc9af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/constants.ts @@ -0,0 +1,4 @@ +export * from './picker-collection/constants.js'; +export * from './picker-item/constants.js'; +export * from './picker-search/constants.js'; +export * from './picker-tree/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/entity-data-picker-data-source.context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/entity-data-picker-data-source.context.token.ts new file mode 100644 index 0000000000..4af8533cfd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/entity-data-picker-data-source.context.token.ts @@ -0,0 +1,9 @@ +import type { UmbEntityDataPickerDataSourceApiContext } from './entity-data-picker-data-source.context.js'; +import type { UmbPickerDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +const contextAlias = 'UmbEntityDataPickerDataSourceApiContext'; + +export const UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT = new UmbContextToken< + UmbEntityDataPickerDataSourceApiContext +>(contextAlias); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/entity-data-picker-data-source.context.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/entity-data-picker-data-source.context.ts new file mode 100644 index 0000000000..08774beafd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/entity-data-picker-data-source.context.ts @@ -0,0 +1,24 @@ +import { UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT } from './entity-data-picker-data-source.context.token.js'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbBasicState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbPickerDataSource } from '@umbraco-cms/backoffice/picker-data-source'; + +export class UmbEntityDataPickerDataSourceApiContext< + DataSourceApiType extends UmbPickerDataSource, +> extends UmbContextBase { + #dataSourceApi = new UmbBasicState(undefined); + public readonly dataSourceApi = this.#dataSourceApi.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT); + } + + setDataSourceApi(dataSourceApi: DataSourceApiType) { + this.#dataSourceApi.setValue(dataSourceApi); + } + + getDataSourceApi(): DataSourceApiType | undefined { + return this.#dataSourceApi.getValue(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/input-entity-data.context.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/input-entity-data.context.ts new file mode 100644 index 0000000000..3b4b62595d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/input-entity-data.context.ts @@ -0,0 +1,179 @@ +import { UMB_ENTITY_DATA_PICKER_COLLECTION_MENU_ALIAS } from '../picker-collection/constants.js'; +import { UMB_ENTITY_DATA_PICKER_TREE_ALIAS } from '../picker-tree/constants.js'; +import { UMB_ENTITY_DATA_PICKER_SEARCH_PROVIDER_ALIAS } from '../picker-search/constants.js'; +import { UMB_ENTITY_DATA_PICKER_ITEM_REPOSITORY_ALIAS } from '../constants.js'; +import { UmbEntityDataPickerDataSourceApiContext } from './entity-data-picker-data-source.context.js'; +import { + UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS, + type UmbCollectionItemPickerModalData, + type UmbCollectionItemPickerModalValue, +} from '@umbraco-cms/backoffice/collection'; +import type { ManifestPropertyEditorDataSource } from '@umbraco-cms/backoffice/property-editor-data-source'; +import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbModalToken, type UmbPickerModalData } from '@umbraco-cms/backoffice/modal'; +import { + UMB_TREE_PICKER_MODAL_ALIAS, + type UmbTreePickerModalData, + type UmbTreePickerModalValue, +} from '@umbraco-cms/backoffice/tree'; +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { + isPickerCollectionDataSource, + isPickerSearchableDataSource, + isPickerTreeDataSource, + type UmbPickerCollectionDataSource, + type UmbPickerDataSource, + type UmbPickerTreeDataSource, +} from '@umbraco-cms/backoffice/picker-data-source'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; +import type { UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils'; + +export class UmbEntityDataPickerInputContext extends UmbPickerInputContext { + #dataSourceAlias?: string; + #dataSourceApiInitializer?: UmbExtensionApiInitializer; + #dataSourceApi?: UmbPickerDataSource; + #dataSourceConfig?: UmbConfigCollectionModel | undefined; + + #dataSourceApiContext = new UmbEntityDataPickerDataSourceApiContext(this); + + constructor(host: UmbControllerHost) { + super(host, UMB_ENTITY_DATA_PICKER_ITEM_REPOSITORY_ALIAS); + } + + /** + * Sets the data source alias for the input context. + * @param {(string | undefined)} alias + * @memberof UmbEntityDataPickerInputContext + */ + setDataSourceAlias(alias: string | undefined) { + this.#dataSourceAlias = alias; + this.#createDataSourceApi(alias); + } + + /** + * Gets the data source alias for the input context. + * @returns {(string | undefined)} The data source alias. + * @memberof UmbEntityDataPickerInputContext + */ + getDataSourceAlias(): string | undefined { + return this.#dataSourceAlias; + } + + /** + * Sets the data source config for the input context. + * @param {(UmbPropertyEditorDataSourceConfigModel | undefined)} config The data source config. + * @memberof UmbEntityDataPickerInputContext + */ + setDataSourceConfig(config: UmbConfigCollectionModel | undefined) { + this.#dataSourceConfig = config; + + if (this.#dataSourceApi) { + this.#dataSourceApi.setConfig?.(config); + } + } + + /** + * Gets the data source config for the input context. + * @returns {(UmbPropertyEditorDataSourceConfigModel | undefined)} The data source config. + * @memberof UmbEntityDataPickerInputContext + */ + getDataSourceConfig(): UmbConfigCollectionModel | undefined { + return this.#dataSourceConfig; + } + + override async openPicker(pickerData?: Partial>) { + this.modalAlias = this.#getModalToken(); + await super.openPicker(pickerData); + } + + #createDataSourceApi(dataSourceAlias: string | undefined) { + if (!dataSourceAlias) { + this.#dataSourceApiInitializer?.destroy(); + return; + } + + this.#dataSourceApiInitializer = new UmbExtensionApiInitializer< + ManifestPropertyEditorDataSource, + UmbExtensionApiInitializer, + UmbPickerDataSource + >(this, umbExtensionsRegistry, dataSourceAlias, [this._host], (permitted, ctrl) => { + if (!permitted) { + // TODO: clean up if not permitted + return; + } + + // TODO: Check if it is a picker data source + this.#dataSourceApi = ctrl.api as UmbPickerDataSource; + this.#dataSourceApi.setConfig?.(this.#dataSourceConfig); + + this.#dataSourceApiContext.setDataSourceApi(this.#dataSourceApi); + }); + } + + #getModalToken() { + if (!this.#dataSourceApi) return; + + const dataSourceApi = this.#dataSourceApi; + + const isTreeDataSource = isPickerTreeDataSource(dataSourceApi); + const isCollectionDataSource = isPickerCollectionDataSource(dataSourceApi); + + // Choose the picker type based on what the data source supports + if (isTreeDataSource) { + return this.#createTreeItemPickerModalToken(dataSourceApi); + } else if (isCollectionDataSource) { + return this.#createCollectionItemPickerModalToken(dataSourceApi); + } else { + throw new Error('The data source API is not a supported type of picker data source.'); + } + } + + #createTreeItemPickerModalToken(api: UmbPickerTreeDataSource) { + const supportsSearch = isPickerSearchableDataSource(api); + + return new UmbModalToken, UmbTreePickerModalValue>( + UMB_TREE_PICKER_MODAL_ALIAS, + { + modal: { + type: 'sidebar', + size: 'small', + }, + data: { + treeAlias: UMB_ENTITY_DATA_PICKER_TREE_ALIAS, + expandTreeRoot: true, + search: supportsSearch + ? { + providerAlias: UMB_ENTITY_DATA_PICKER_SEARCH_PROVIDER_ALIAS, + } + : undefined, + }, + }, + ); + } + + #createCollectionItemPickerModalToken(api: UmbPickerCollectionDataSource) { + const supportsSearch = isPickerSearchableDataSource(api); + + return new UmbModalToken, UmbCollectionItemPickerModalValue>( + UMB_COLLECTION_ITEM_PICKER_MODAL_ALIAS, + { + modal: { + type: 'sidebar', + size: 'small', + }, + data: { + collection: { + menuAlias: UMB_ENTITY_DATA_PICKER_COLLECTION_MENU_ALIAS, + }, + search: supportsSearch + ? { + providerAlias: UMB_ENTITY_DATA_PICKER_SEARCH_PROVIDER_ALIAS, + } + : undefined, + }, + }, + ); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/input-entity-data.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/input-entity-data.element.ts new file mode 100644 index 0000000000..08514a7729 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/input/input-entity-data.element.ts @@ -0,0 +1,237 @@ +import { UmbEntityDataPickerInputContext } from './input-entity-data.context.js'; +import { css, html, customElement, property, state, repeat, nothing, when } from '@umbraco-cms/backoffice/external/lit'; +import { splitStringToArray, type UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbRepositoryItemsStatus } from '@umbraco-cms/backoffice/repository'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; + +@customElement('umb-input-entity-data') +export class UmbInputEntityDataElement extends UUIFormControlMixin(UmbLitElement, '') { + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => { + return element.id; + }, + getUniqueOfModel: (modelEntry) => { + return modelEntry; + }, + identifier: 'Umb.SorterIdentifier.InputEntityData', + itemSelector: 'umb-entity-item-ref', + containerSelector: 'uui-ref-list', + onChange: ({ model }) => { + this.selection = model; + this.dispatchEvent(new UmbChangeEvent()); + }, + }); + + public set dataSourceAlias(value: string | undefined) { + this.#pickerInputContext.setDataSourceAlias(value); + } + public get dataSourceAlias(): string | undefined { + return this.#pickerInputContext.getDataSourceAlias(); + } + + public set dataSourceConfig(config: UmbConfigCollectionModel | undefined) { + this.#pickerInputContext.setDataSourceConfig(config); + } + public get dataSourceConfig(): UmbConfigCollectionModel | undefined { + return this.#pickerInputContext.getDataSourceConfig(); + } + + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default + */ + @property({ type: Number }) + public set min(value: number) { + this.#pickerInputContext.min = value; + } + public get min(): number { + return this.#pickerInputContext.min; + } + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default + */ + @property({ type: Number }) + public set max(value: number) { + this.#pickerInputContext.max = value; + } + public get max(): number { + return this.#pickerInputContext.max; + } + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'max-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + @property({ type: Array }) + public set selection(uniques: Array) { + this.#pickerInputContext.setSelection(uniques); + this.#sorter.setModel(uniques); + } + public get selection(): Array { + return this.#pickerInputContext.getSelection(); + } + + @property() + public override set value(uniques: string) { + this.selection = splitStringToArray(uniques); + } + public override get value(): string { + return this.selection.join(','); + } + + /** + * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. + * @type {boolean} + * @attr + * @default + */ + @property({ type: Boolean, reflect: true }) + public get readonly() { + return this.#readonly; + } + public set readonly(value) { + this.#readonly = value; + + if (this.#readonly) { + this.#sorter.disable(); + } else { + this.#sorter.enable(); + } + } + #readonly = false; + + @state() + private _items: Array = []; + + @state() + private _statuses?: Array; + + #pickerInputContext = new UmbEntityDataPickerInputContext(this); + + constructor() { + super(); + + this.addValidator( + 'rangeUnderflow', + () => this.minMessage, + () => !!this.min && this.#pickerInputContext.getSelection().length < this.min, + ); + + this.addValidator( + 'rangeOverflow', + () => this.maxMessage, + () => !!this.max && this.#pickerInputContext.getSelection().length > this.max, + ); + + this.observe( + this.#pickerInputContext.selection, + (selection) => (this.value = selection.join(',')), + '_observeSelection', + ); + + this.observe( + this.#pickerInputContext.selectedItems, + (selectedItems) => (this._items = selectedItems), + '_observerItems', + ); + + this.observe(this.#pickerInputContext.statuses, (statuses) => (this._statuses = statuses), '_observerStatuses'); + } + + protected override getFormElement() { + return undefined; + } + + #onRemove(unique: string) { + this.#pickerInputContext.requestRemoveItem(unique); + } + + override render() { + return html`${this.#renderItems()} ${this.#renderAddButton()}`; + } + + #renderAddButton() { + if (this.max > 0 && this.selection.length >= this.max) return nothing; + if (this.readonly && this.selection.length > 0) return nothing; + return html` + this.#pickerInputContext.openPicker()} + label="${this.localize.term('general_choose')}" + ?disabled=${this.readonly}> + `; + } + + #renderItems() { + if (!this._statuses) return; + return html` + + ${repeat( + this._statuses, + (status) => status.unique, + (status) => { + const unique = status.unique; + const item = this._items?.find((x) => x.unique === unique); + return html` + ${when( + !this.readonly, + () => html` + + this.#onRemove(unique)}> + + `, + )} + `; + }, + )} + + `; + } + + static override styles = [ + css` + #btn-add { + width: 100%; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-entity-data': UmbInputEntityDataElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/manifests.ts new file mode 100644 index 0000000000..5f995de664 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/manifests.ts @@ -0,0 +1,13 @@ +import { manifests as pickerCollectionMenuManifests } from './picker-collection/manifests.js'; +import { manifests as pickerItemManifests } from './picker-item/manifests.js'; +import { manifests as pickerSearchManifests } from './picker-search/manifests.js'; +import { manifests as pickerTreeManifests } from './picker-tree/manifests.js'; +import { manifests as propertyEditorManifests } from './property-editor/manifests.js'; + +export const manifests: Array = [ + ...pickerCollectionMenuManifests, + ...pickerItemManifests, + ...pickerSearchManifests, + ...pickerTreeManifests, + ...propertyEditorManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/constants.ts new file mode 100644 index 0000000000..c49bfcc2d9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/constants.ts @@ -0,0 +1,2 @@ +export const UMB_ENTITY_DATA_PICKER_COLLECTION_MENU_ALIAS = 'Umb.collectionMenu.EntityDataPicker'; +export const UMB_ENTITY_DATA_PICKER_COLLECTION_REPOSITORY_ALIAS = 'Umb.Repository.EntityDataPickerCollection'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/entity-data-picker-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/entity-data-picker-collection.repository.ts new file mode 100644 index 0000000000..382134e4c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/entity-data-picker-collection.repository.ts @@ -0,0 +1,40 @@ +import { UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT } from '../input/entity-data-picker-data-source.context.token.js'; +import type { UmbCollectionFilterModel, UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbPickerCollectionDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbEntityDataPickerCollectionRepository + extends UmbRepositoryBase + implements UmbCollectionRepository, UmbApi +{ + #init: Promise<[any]>; + #pickerDataSourceContext?: typeof UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.#init = Promise.all([ + this.consumeContext(UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT, (context) => { + this.#pickerDataSourceContext = context; + }).asPromise(), + ]); + } + + async requestCollection(filter: UmbCollectionFilterModel) { + await this.#init; + const api = await this.#getApi(); + return api.requestCollection(filter); + } + + async #getApi() { + const api = (await this.observe( + this.#pickerDataSourceContext?.dataSourceApi, + )?.asPromise()) as UmbPickerCollectionDataSource; + if (!api) throw new Error('No data source API set'); + return api; + } +} + +export { UmbEntityDataPickerCollectionRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/manifests.ts new file mode 100644 index 0000000000..5382b02974 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-collection/manifests.ts @@ -0,0 +1,22 @@ +import { + UMB_ENTITY_DATA_PICKER_COLLECTION_MENU_ALIAS, + UMB_ENTITY_DATA_PICKER_COLLECTION_REPOSITORY_ALIAS, +} from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: UMB_ENTITY_DATA_PICKER_COLLECTION_REPOSITORY_ALIAS, + name: 'Entity Data Picker Collection Repository', + api: () => import('./entity-data-picker-collection.repository.js'), + }, + { + type: 'collectionMenu', + kind: 'default', + alias: UMB_ENTITY_DATA_PICKER_COLLECTION_MENU_ALIAS, + name: 'Entity Data Picker Collection Menu', + meta: { + collectionRepositoryAlias: UMB_ENTITY_DATA_PICKER_COLLECTION_REPOSITORY_ALIAS, + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/constants.ts new file mode 100644 index 0000000000..898592120c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/constants.ts @@ -0,0 +1 @@ +export const UMB_ENTITY_DATA_PICKER_ITEM_REPOSITORY_ALIAS = 'Umb.Repository.EntityDataPickerItem'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/entity-data-picker-item.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/entity-data-picker-item.repository.ts new file mode 100644 index 0000000000..e6ee64bffe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/entity-data-picker-item.repository.ts @@ -0,0 +1,37 @@ +import { UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT } from '../input/entity-data-picker-data-source.context.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import { UmbRepositoryBase, type UmbItemRepository } from '@umbraco-cms/backoffice/repository'; + +export class UmbEntityDataPickerItemRepository + extends UmbRepositoryBase + implements UmbItemRepository, UmbApi +{ + #init: Promise<[any]>; + #pickerDataSourceContext?: typeof UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.#init = Promise.all([ + this.consumeContext(UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT, (context) => { + this.#pickerDataSourceContext = context; + }).asPromise(), + ]); + } + + async requestItems(uniques: Array) { + await this.#init; + const api = await this.#getApi(); + return api.requestItems(uniques); + } + + async #getApi() { + const api = await this.observe(this.#pickerDataSourceContext?.dataSourceApi)?.asPromise(); + if (!api) throw new Error('No data source API set'); + return api; + } +} + +export { UmbEntityDataPickerItemRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/manifests.ts new file mode 100644 index 0000000000..8b7fda2c2c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-item/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_ENTITY_DATA_PICKER_ITEM_REPOSITORY_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'repository', + alias: UMB_ENTITY_DATA_PICKER_ITEM_REPOSITORY_ALIAS, + name: 'Entity Data Picker Item Repository', + api: () => import('./entity-data-picker-item.repository.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/constants.ts new file mode 100644 index 0000000000..d1a84f9925 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/constants.ts @@ -0,0 +1 @@ +export const UMB_ENTITY_DATA_PICKER_SEARCH_PROVIDER_ALIAS = 'Umb.SearchProvider.EntityDataPicker'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/entity-data-picker.search-provider.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/entity-data-picker.search-provider.ts new file mode 100644 index 0000000000..7bde770a79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/entity-data-picker.search-provider.ts @@ -0,0 +1,40 @@ +import { UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT } from '../input/entity-data-picker-data-source.context.token.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbPickerSearchableDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import type { UmbSearchProvider, UmbSearchRequestArgs, UmbSearchResultItemModel } from '@umbraco-cms/backoffice/search'; + +export class UmbEntityDataPickerSearchProvider + extends UmbControllerBase + implements UmbSearchProvider, UmbApi +{ + #init: Promise<[any]>; + #pickerDataSourceContext?: typeof UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.#init = Promise.all([ + this.consumeContext(UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT, (context) => { + this.#pickerDataSourceContext = context; + }).asPromise(), + ]); + } + + async search(args: UmbSearchRequestArgs) { + await this.#init; + const api = await this.#getApi(); + return api.search?.(args); + } + + async #getApi() { + const api = (await this.observe(this.#pickerDataSourceContext?.dataSourceApi)?.asPromise()) as + | UmbPickerSearchableDataSource + | undefined; + if (!api) throw new Error('No data source API set'); + return api; + } +} + +export { UmbEntityDataPickerSearchProvider as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/manifests.ts new file mode 100644 index 0000000000..99da18f628 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-search/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_ENTITY_DATA_PICKER_SEARCH_PROVIDER_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'searchProvider', + alias: UMB_ENTITY_DATA_PICKER_SEARCH_PROVIDER_ALIAS, + name: 'Entity Data Picker Search Provider', + api: () => import('./entity-data-picker.search-provider.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/constants.ts new file mode 100644 index 0000000000..eb6c10fd47 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/constants.ts @@ -0,0 +1 @@ +export const UMB_ENTITY_DATA_PICKER_TREE_ALIAS = 'Umb.Tree.EntityDataPicker'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/entity-data-picker-tree.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/entity-data-picker-tree.repository.ts new file mode 100644 index 0000000000..39ee8a1d50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/entity-data-picker-tree.repository.ts @@ -0,0 +1,60 @@ +import { UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT } from '../input/entity-data-picker-data-source.context.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbPickerTreeDataSource } from '@umbraco-cms/backoffice/picker-data-source'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { + UmbTreeAncestorsOfRequestArgs, + UmbTreeChildrenOfRequestArgs, + UmbTreeRepository, + UmbTreeRootItemsRequestArgs, +} from '@umbraco-cms/backoffice/tree'; + +export class UmbEntityDataPickerTreeRepository extends UmbRepositoryBase implements UmbTreeRepository, UmbApi { + #init: Promise<[any]>; + #pickerDataSourceContext?: typeof UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.#init = Promise.all([ + this.consumeContext(UMB_ENTITY_DATA_PICKER_DATA_SOURCE_API_CONTEXT, (context) => { + this.#pickerDataSourceContext = context; + }).asPromise(), + ]); + } + + async requestTreeRoot() { + await this.#init; + const api = await this.#getApi(); + return api.requestTreeRoot(); + } + + async requestTreeRootItems(args: UmbTreeRootItemsRequestArgs) { + await this.#init; + const api = await this.#getApi(); + return api.requestTreeRootItems(args); + } + + async requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) { + await this.#init; + const api = await this.#getApi(); + return api.requestTreeItemsOf(args); + } + + async requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) { + await this.#init; + const api = await this.#getApi(); + return api.requestTreeItemAncestors(args); + } + + async #getApi() { + const api = (await this.observe( + this.#pickerDataSourceContext?.dataSourceApi, + )?.asPromise()) as UmbPickerTreeDataSource; + if (!api) throw new Error('No data source API set'); + return api; + } +} + +export { UmbEntityDataPickerTreeRepository as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/manifests.ts new file mode 100644 index 0000000000..fb6869c07a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/picker-tree/manifests.ts @@ -0,0 +1,21 @@ +import { UMB_ENTITY_DATA_PICKER_TREE_ALIAS } from './constants.js'; + +const repositoryAlias = 'Umb.Repository.EntityDataPickerTree'; + +export const manifests: Array = [ + { + type: 'repository', + alias: repositoryAlias, + name: 'Entity Data Picker Tree Repository', + api: () => import('./entity-data-picker-tree.repository.js'), + }, + { + type: 'tree', + kind: 'default', + alias: UMB_ENTITY_DATA_PICKER_TREE_ALIAS, + name: 'Entity Data Picker Tree', + meta: { + repositoryAlias, + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/entity-data-picker-property-editor-ui.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/entity-data-picker-property-editor-ui.element.ts new file mode 100644 index 0000000000..71d5e95c7f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/entity-data-picker-property-editor-ui.element.ts @@ -0,0 +1,139 @@ +import type { UmbInputEntityDataElement } from '../input/input-entity-data.element.js'; +import type { UmbEntityDataPickerPropertyEditorValue } from './types.js'; +import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import type { + UmbPropertyEditorConfigCollection, + UmbPropertyEditorUiElement, +} from '@umbraco-cms/backoffice/property-editor'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; + +// import of local component +import '../input/input-entity-data.element.js'; +import type { UmbConfigCollectionModel } from '@umbraco-cms/backoffice/utils'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestPropertyEditorDataSource } from '@umbraco-cms/backoffice/property-editor-data-source'; +import type { UmbNumberRangeValueType } from '@umbraco-cms/backoffice/models'; + +@customElement('umb-entity-data-picker-property-editor-ui') +export class UmbEntityDataPickerPropertyEditorUIElement + extends UmbFormControlMixin( + UmbLitElement, + undefined, + ) + implements UmbPropertyEditorUiElement +{ + /** + * The data source alias to use for this property editor. + * @type {string} + * @memberof UmbEntityDataPickerPropertyEditorUIElement + */ + @property({ type: String }) + private _dataSourceAlias?: string | undefined; + public get dataSourceAlias(): string | undefined { + return this._dataSourceAlias; + } + public set dataSourceAlias(value: string | undefined) { + this._dataSourceAlias = value; + this.#extractDataSourceConfig(); + } + + /** + * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. + * @type {boolean} + * @attr + * @default false + */ + @property({ type: Boolean, reflect: true }) + readonly = false; + + @state() + private _min = 0; + + @state() + private _minMessage = ''; + + @state() + private _max = Infinity; + + @state() + private _maxMessage = ''; + + @state() + private _dataSourceConfig?: UmbConfigCollectionModel; + + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + this.#propertyEditorConfigCollection = config; + + const minMax = config?.getValueByAlias('validationLimit'); + this._min = minMax?.min ?? 0; + this._max = minMax?.max ?? Infinity; + + this._minMessage = `${this.localize.term('validation_minCount')} ${this._min} ${this.localize.term('validation_items')}`; + this._maxMessage = `${this.localize.term('validation_maxCount')} ${this._max} ${this.localize.term('validation_itemsSelected')}`; + + this._dataSourceConfig = this.#extractDataSourceConfig(); + } + + #propertyEditorConfigCollection?: UmbPropertyEditorConfigCollection; + + #extractDataSourceConfig() { + if (!this._dataSourceAlias || !this.#propertyEditorConfigCollection) { + this._dataSourceConfig = undefined; + return; + } + + const dataSourceExtension = umbExtensionsRegistry.getByAlias( + this._dataSourceAlias, + ); + + if (!dataSourceExtension) { + throw new Error(`Data source with alias ${this._dataSourceAlias} not found`); + } + + const aliases = dataSourceExtension.meta?.settings?.properties.map((property) => property.alias); + const configAliasMatch = this.#propertyEditorConfigCollection.filter((configEntry) => + aliases?.includes(configEntry.alias), + ); + + const dataSourceConfig: UmbConfigCollectionModel | undefined = configAliasMatch?.map((configEntry) => { + return { + alias: configEntry.alias, + value: configEntry.value, + }; + }); + + return dataSourceConfig; + } + + override focus() { + return this.shadowRoot?.querySelector('umb-input-entity-data')?.focus(); + } + + #onChange(event: CustomEvent & { target: UmbInputEntityDataElement }) { + this.value = event.target.selection; + this.dispatchEvent(new UmbChangeEvent()); + } + + override render() { + return html``; + } +} + +export { UmbEntityDataPickerPropertyEditorUIElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-entity-data-property-editor-ui': UmbEntityDataPickerPropertyEditorUIElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/manifests.ts new file mode 100644 index 0000000000..677f71af17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/manifests.ts @@ -0,0 +1,33 @@ +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; + +const manifest: ManifestPropertyEditorUi = { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.EntityDataPicker', + name: 'Entity Data Picker Property Editor UI', + element: () => import('./entity-data-picker-property-editor-ui.element.js'), + meta: { + label: 'Entity Data Picker', + icon: 'icon-page-add', + group: 'pickers', + propertyEditorSchemaAlias: 'Umbraco.Plain.Json', + supportsReadOnly: true, + supportsDataSource: { + enabled: true, + forDataSourceTypes: ['picker'], + }, + settings: { + properties: [ + // TODO: Move this to schema manifest when server can validate it + { + alias: 'validationLimit', + label: 'Amount', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.NumberRange', + config: [{ alias: 'validationRange', value: { min: 0, max: Infinity } }], + weight: 100, + }, + ], + }, + }, +}; + +export const manifests: Array = [manifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/types.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/types.ts new file mode 100644 index 0000000000..fa9bbf6564 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/entity-data-picker/property-editor/types.ts @@ -0,0 +1 @@ +export type UmbEntityDataPickerPropertyEditorValue = Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts index 948ce907b6..b464b9d80a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/manifests.ts @@ -25,6 +25,7 @@ import { manifests as textareaManifests } from './textarea/manifests.js'; import { manifests as textBoxManifests } from './text-box/manifests.js'; import { manifests as toggleManifests } from './toggle/manifests.js'; import { manifests as contentPickerManifests } from './content-picker/manifests.js'; +import { manifests as entityDataPickerManifests } from './entity-data-picker/manifests.js'; export const manifests: Array = [ ...checkboxListManifests, @@ -45,6 +46,7 @@ export const manifests: Array = [ ...textBoxManifests, ...toggleManifests, ...contentPickerManifests, + ...entityDataPickerManifests, acceptedType, colorEditor, dimensions, diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/package.json b/src/Umbraco.Web.UI.Client/src/packages/search/package.json deleted file mode 100644 index dce223609a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/search/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@umbraco-backoffice/search", - "private": true, - "type": "module", - "scripts": { - "build": "vite build" - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/search/umbraco-package.ts deleted file mode 100644 index d669951eaa..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/search/umbraco-package.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const name = 'Umbraco.Core.Search'; -export const extensions = [ - { - name: 'Search Bundle', - alias: 'Umb.Bundle.Search', - type: 'bundle', - js: () => import('./manifests.js'), - }, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/search/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/search/vite.config.ts deleted file mode 100644 index ce6964cd3b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/search/vite.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vite'; -import { rmSync } from 'fs'; -import { getDefaultConfig } from '../../vite-config-base'; - -const dist = '../../../dist-cms/packages/search'; - -// delete the unbundled dist folder -rmSync(dist, { recursive: true, force: true }); - -export default defineConfig({ - ...getDefaultConfig({ dist }), -}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/entity.ts new file mode 100644 index 0000000000..06e41eb0be --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/entity.ts @@ -0,0 +1,3 @@ +export const UMB_STATIC_FILE_ENTITY_TYPE = 'static-file'; + +export type UmbStaticFileEntityType = typeof UMB_STATIC_FILE_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts index 82e552edd2..6c41bc552e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/static-file-item.server.data-source.ts @@ -1,3 +1,4 @@ +import { UMB_STATIC_FILE_ENTITY_TYPE } from '../../entity.js'; import type { UmbStaticFileItemModel } from './types.js'; import { UmbManagementApiStaticFileItemDataRequestManager } from './static-file-item.server.request-manager.js'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; @@ -41,6 +42,7 @@ export class UmbStaticFileItemServerDataSource extends UmbItemServerDataSourceBa const mapper = (item: StaticFileItemResponseModel): UmbStaticFileItemModel => { const serializer = new UmbServerFilePathUniqueSerializer(); return { + entityType: UMB_STATIC_FILE_ENTITY_TYPE, isFolder: item.isFolder, name: item.name, parentUnique: item.parent ? serializer.toUnique(item.parent.path) : null, diff --git a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/types.ts index 58c91fcabd..ca1fc3df39 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/static-file/repository/item/types.ts @@ -1,4 +1,7 @@ +/* TODO: This interface has a parenUnique. It looks more like a Tree Item than an Item? Investigate how this is used, +rename and extend The correct base model (tree or item) */ export interface UmbStaticFileItemModel { + entityType: string; isFolder: boolean; name: string; parentUnique: string | null; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.context.ts index 9ef3ef4b6f..87a84c215c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.context.ts @@ -1,14 +1,15 @@ import { UMB_STYLESHEET_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbStylesheetItemModel } from '../../types.js'; +import type { UmbStylesheetTreeItemModel } from '../../tree/types.js'; import { UMB_STYLESHEET_PICKER_MODAL } from './stylesheet-picker-modal.token.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbStylesheetPickerInputContext extends UmbPickerInputContext { +export class UmbStylesheetPickerInputContext extends UmbPickerInputContext< + UmbStylesheetItemModel, + UmbStylesheetTreeItemModel +> { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: Item and tree item types collide super(host, UMB_STYLESHEET_ITEM_REPOSITORY_ALIAS, UMB_STYLESHEET_PICKER_MODAL); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/modals/table-properties-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/modals/table-properties-modal.element.ts index 33c5a127cb..82122ab203 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/modals/table-properties-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/modals/table-properties-modal.element.ts @@ -2,7 +2,7 @@ import type { UmbTiptapTablePropertiesModalData, UmbTiptapTablePropertiesModalValue, } from './table-properties-modal.token.js'; -import { css, customElement, html, repeat, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, ifDefined, repeat, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import type { UmbPropertyDatasetElement, UmbPropertyValueData } from '@umbraco-cms/backoffice/property'; import type { UmbPropertyEditorConfig } from '@umbraco-cms/backoffice/property-editor'; @@ -13,6 +13,7 @@ type UmbProperty = { description?: string; label: string; propertyEditorUiAlias: string; + propertyEditorDataSourceAlias?: string; }; @customElement('umb-tiptap-table-properties-modal') @@ -113,6 +114,7 @@ export class UmbTiptapTablePropertiesModalElement extends UmbModalBaseElement< alias=${property.alias} label=${property.label} property-editor-ui-alias=${property.propertyEditorUiAlias} + property-editor-data-source-alias=${ifDefined(property.propertyEditorDataSourceAlias)} .appearance=${this.#appearance} .config=${property.config}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/types.ts index 3a7b84b6d9..4f145f9920 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/types.ts @@ -1,5 +1,6 @@ -export interface UmbUserGroupItemModel { - unique: string; +import type { UmbItemModel } from '@umbraco-cms/backoffice/entity-item'; + +export interface UmbUserGroupItemModel extends UmbItemModel { name: string; icon: string | null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts index 16055e1aa5..ff82509188 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/item/user-group-item.server.data-source.ts @@ -1,3 +1,4 @@ +import { UMB_USER_GROUP_ENTITY_TYPE } from '../../entity.js'; import type { UmbUserGroupItemModel } from './types.js'; import { UmbManagementApiUserGroupItemDataRequestManager } from './user-group-item.server.request-manager.js'; import type { UserGroupItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; @@ -37,6 +38,7 @@ export class UmbUserGroupItemServerDataSource extends UmbItemServerDataSourceBas const mapper = (item: UserGroupItemResponseModel): UmbUserGroupItemModel => { return { + entityType: UMB_USER_GROUP_ENTITY_TYPE, unique: item.id, name: item.name, icon: item.icon || null, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.context.ts index 007d716eec..ead8931e58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/components/user-input/user-input.context.ts @@ -1,10 +1,10 @@ -import { UMB_USER_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; +import { UMB_USER_ITEM_REPOSITORY_ALIAS, type UmbUserItemModel } from '../../repository/index.js'; import type { UmbUserDetailModel } from '../../types.js'; import { UMB_USER_PICKER_MODAL } from '../../modals/user-picker/user-picker-modal.token.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbUserPickerInputContext extends UmbPickerInputContext { +export class UmbUserPickerInputContext extends UmbPickerInputContext { constructor(host: UmbControllerHost) { super(host, UMB_USER_ITEM_REPOSITORY_ALIAS, UMB_USER_PICKER_MODAL); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/entity.ts index 51932a19c6..b04b01a0b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/entity.ts @@ -4,6 +4,7 @@ export const UMB_WEBHOOK_WORKSPACE_ALIAS = 'Umb.Workspace.Webhook'; */ export const UMB_WEBHOOK_WORKSPACE = 'Umb.Workspace.Webhook'; +// TODO: move into each module export const UMB_WEBHOOK_ENTITY_TYPE = 'webhook'; export const UMB_WEBHOOK_ROOT_ENTITY_TYPE = 'webhook-root'; export const UMB_WEBHOOK_DELIVERY_ENTITY_TYPE = 'webhook-delivery'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/types.ts index de42a3a1e5..ecfc2b0c2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/types.ts @@ -1,4 +1,5 @@ -export interface UmbWebhookItemModel { +import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbWebhookItemModel extends UmbNamedEntityModel { unique: string; - name: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts index f6115c194f..ca65024f61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/webhook/webhook/repository/item/webhook-item.server.data-source.ts @@ -1,3 +1,4 @@ +import { UMB_WEBHOOK_ENTITY_TYPE } from '../../../entity.js'; import type { UmbWebhookItemModel } from './types.js'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; import type { WebhookItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; @@ -42,7 +43,8 @@ export class UmbWebhookItemServerDataSource extends UmbItemServerDataSourceBase< const mapper = (item: WebhookItemResponseModel): UmbWebhookItemModel => { return { - unique: item.name, + unique: item.id, name: item.name, + entityType: UMB_WEBHOOK_ENTITY_TYPE, }; }; diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index ef64a1e410..e1928fbeec 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -105,14 +105,19 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/menu": ["./src/packages/core/menu/index.ts"], "@umbraco-cms/backoffice/modal": ["./src/packages/core/modal/index.ts"], "@umbraco-cms/backoffice/models": ["./src/packages/core/models/index.ts"], + "@umbraco-cms/backoffice/search": ["./src/packages/core/search/index.ts"], "@umbraco-cms/backoffice/multi-url-picker": ["./src/packages/multi-url-picker/index.ts"], "@umbraco-cms/backoffice/notification": ["./src/packages/core/notification/index.ts"], "@umbraco-cms/backoffice/object-type": ["./src/packages/core/object-type/index.ts"], "@umbraco-cms/backoffice/package": ["./src/packages/packages/package/index.ts"], "@umbraco-cms/backoffice/partial-view": ["./src/packages/templating/partial-views/index.ts"], "@umbraco-cms/backoffice/picker-input": ["./src/packages/core/picker-input/index.ts"], + "@umbraco-cms/backoffice/picker-data-source": ["./src/packages/core/picker-data-source/index.ts"], "@umbraco-cms/backoffice/picker": ["./src/packages/core/picker/index.ts"], "@umbraco-cms/backoffice/property-action": ["./src/packages/core/property-action/index.ts"], + "@umbraco-cms/backoffice/property-editor-data-source": [ + "./src/packages/core/property-editor-data-source/index.ts" + ], "@umbraco-cms/backoffice/property-editor": ["./src/packages/core/property-editor/index.ts"], "@umbraco-cms/backoffice/property-type": ["./src/packages/content/property-type/index.ts"], "@umbraco-cms/backoffice/property": ["./src/packages/core/property/index.ts"], @@ -124,7 +129,6 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/router": ["./src/packages/core/router/index.ts"], "@umbraco-cms/backoffice/rte": ["./src/packages/rte/index.ts"], "@umbraco-cms/backoffice/script": ["./src/packages/templating/scripts/index.ts"], - "@umbraco-cms/backoffice/search": ["./src/packages/search/index.ts"], "@umbraco-cms/backoffice/section": ["./src/packages/core/section/index.ts"], "@umbraco-cms/backoffice/segment": ["./src/packages/segment/index.ts"], "@umbraco-cms/backoffice/server-file-system": ["./src/packages/core/server-file-system/index.ts"],