From 3c6f222c8bb51331e74b27bb6ddcb1600c4dfd3b Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Thu, 10 Jul 2025 10:33:56 +0100 Subject: [PATCH] Fixes Block Catalogue Modal Filter (#19700) * Mock data updates The `icon` is not part the block-type data. * Adds `description` to the mock doctype model * Refactors block catalogue modal to make the filter/search work with a block-type's name & description. This removes the need to use the `` component, all element-type data is requested upfront. * Reverted dev/debug change * Abstracted out the element-type items observation to its own method * Updated CSS rule thanks to a Copilot suggestion. --- .../mocks/data/data-type/data-type.data.ts | 8 +- .../data/document-type/document-type.data.ts | 2 +- .../data/document-type/document-type.db.ts | 1 + ...i-block-grid-type-configuration.element.ts | 3 + ...i-block-list-type-configuration.element.ts | 4 +- .../block-catalogue-modal.element.ts | 165 ++++++++++++++---- 6 files changed, 137 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 571074884e..3439651914 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -624,7 +624,8 @@ export const data: Array = [ label: 'Mocked Block Type for Block List', contentElementTypeKey: '4f68ba66-6fb2-4778-83b8-6ab4ca3a7c5c', settingsElementTypeKey: 'all-property-editors-document-type-id', - icon: 'icon-server-alt', + iconColor: '#F5C1BC', + backgroundColor: '#1B264F', }, { label: 'Mocked Coffee Block', @@ -632,7 +633,6 @@ export const data: Array = [ iconColor: '#FFFDD0', backgroundColor: '#633f32', editorSize: 'medium', - icon: 'icon-coffee', }, { label: 'Headline', @@ -640,25 +640,21 @@ export const data: Array = [ settingsElementTypeKey: 'headline-settings-demo-block-id', backgroundColor: 'gold', editorSize: 'medium', - icon: 'icon-edit', }, { label: 'Image', contentElementTypeKey: 'image-umbraco-demo-block-id', editorSize: 'medium', - icon: 'icon-picture', }, { label: 'Rich Text', contentElementTypeKey: 'rich-text-umbraco-demo-block-id', editorSize: 'medium', - icon: 'icon-diploma', }, { label: 'Two Column Layout', contentElementTypeKey: 'two-column-layout-umbraco-demo-block-id', editorSize: 'medium', - icon: 'icon-book-alt', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index 7314cc77d3..d9f0c7ae5a 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -1405,7 +1405,7 @@ export const data: Array = [ id: 'coffee-umbraco-demo-block-id', alias: 'coffeeUmbracoDemoBlock', name: 'Favorite Coffee', - description: null, + description: 'The delicious taste of coffee.', icon: 'icon-coffee', allowedAsRoot: true, variesByCulture: false, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.db.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.db.ts index 24a362a1cb..236eaada38 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.db.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.db.ts @@ -140,6 +140,7 @@ const documentTypeItemMapper = (item: UmbMockDocumentTypeModel): DocumentTypeIte name: item.name, icon: item.icon, isElement: item.isElement, + description: item.description ?? undefined, }; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts index 883aa25d78..ea43807b9d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts @@ -29,6 +29,9 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +// TODO: This is across packages, how should we go about getting just a single element from another package? like here we just need the `umb-input-block-type` element. +import '@umbraco-cms/backoffice/block-type'; + interface MappedGroupWithBlockTypes extends UmbBlockGridTypeGroupType { blocks: Array; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts index db98ad1626..c7e679c6fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-type-configuration/property-editor-ui-block-list-type-configuration.element.ts @@ -1,4 +1,3 @@ -import '../../../block-type/components/input-block-type/index.js'; import { UMB_BLOCK_LIST_TYPE } from '../../constants.js'; import type { UmbBlockTypeBaseModel, UmbInputBlockTypeElement } from '@umbraco-cms/backoffice/block-type'; import type { @@ -11,6 +10,9 @@ import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +// TODO: This is across packages, how should we go about getting just a single element from another package? like here we just need the `umb-input-block-type` element. +import '@umbraco-cms/backoffice/block-type'; + /** * @element umb-property-editor-ui-block-list-type-configuration */ diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts index 92e8cc60ba..ef818637f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/modals/block-catalogue/block-catalogue-modal.element.ts @@ -1,24 +1,44 @@ import { UMB_BLOCK_WORKSPACE_MODAL } from '../../workspace/index.js'; import { UMB_BLOCK_MANAGER_CONTEXT } from '../../context/index.js'; import type { UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue } from './block-catalogue-modal.token.js'; -import type { UmbBlockTypeGroup, UmbBlockTypeWithGroupKey } from '@umbraco-cms/backoffice/block-type'; -import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UMB_MODAL_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { + css, + customElement, + html, + ifDefined, + nothing, + repeat, + state, + when, +} from '@umbraco-cms/backoffice/external/lit'; +import { transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; +import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; +import { UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/document-type'; +import { UMB_MODAL_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; +import type { UmbBlockTypeGroup, UmbBlockTypeWithGroupKey } from '@umbraco-cms/backoffice/block-type'; +import type { UmbDocumentTypeItemModel } from '@umbraco-cms/backoffice/document-type'; import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; -// TODO: This is across packages, how should we go about getting just a single element from another package? like here we just need the umb-block-type-card element -import '@umbraco-cms/backoffice/block-type'; +type UmbBlockTypeItemWithGroupKey = UmbBlockTypeWithGroupKey & UmbDocumentTypeItemModel; @customElement('umb-block-catalogue-modal') export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue > { + readonly #itemManager = new UmbRepositoryItemsManager( + this, + UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, + ); + #search = ''; - private _groupedBlocks: Array<{ name?: string; blocks: Array }> = []; + #serverUrl = ''; + + private _groupedBlocks: Array<{ name?: string; blocks: Array }> = []; @state() private _openClipboard?: boolean; @@ -27,14 +47,21 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< private _workspacePath?: string; @state() - private _filtered: Array<{ name?: string; blocks: Array }> = []; + private _filtered: Array<{ name?: string; blocks: Array }> = []; @state() _manager?: typeof UMB_BLOCK_MANAGER_CONTEXT.TYPE; + @state() + _loading = true; + constructor() { super(); + this.consumeContext(UMB_SERVER_CONTEXT, (instance) => { + this.#serverUrl = instance?.getServerUrl() ?? ''; + }); + this.consumeContext(UMB_MODAL_CONTEXT, (modalContext) => { if (modalContext?.data.createBlockInWorkspace) { new UmbModalRouteRegistrationController(this, UMB_BLOCK_WORKSPACE_MODAL) @@ -57,6 +84,10 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< this.consumeContext(UMB_BLOCK_MANAGER_CONTEXT, (manager) => { this._manager = manager; }); + + this.observe(this.#itemManager.items, async (items) => { + this.#observeBlockTypes(items); + }); } override connectedCallback() { @@ -65,17 +96,37 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< this._openClipboard = this.data.openClipboard ?? false; - const blocks: Array = this.data.blocks ?? []; - const blockGroups: Array = this.data.blockGroups ?? []; + this.#itemManager.setUniques(this.data.blocks.map((block) => block.contentElementTypeKey)); + } + + #observeBlockTypes(items: Array | undefined) { + if (!items?.length) return; + + const lookup = items.reduce( + (acc, item) => { + acc[item.unique] = item; + return acc; + }, + {} as { [key: string]: UmbDocumentTypeItemModel }, + ); + + const blocks: Array = + this.data?.blocks?.map((block) => ({ ...(lookup[block.contentElementTypeKey] ?? {}), ...block })) ?? []; + + const blockGroups: Array = this.data?.blockGroups ?? []; const noGroupBlocks = blocks.filter((block) => !blockGroups.find((group) => group.key === block.groupKey)); + const grouped = blockGroups.map((group) => ({ name: group.name, blocks: blocks.filter((block) => block.groupKey === group.key), })); this._groupedBlocks = [{ blocks: noGroupBlocks }, ...grouped]; + this.#updateFiltered(); + + this._loading = false; } #updateFiltered() { @@ -84,7 +135,15 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< } else { const search = this.#search.toLowerCase(); this._filtered = this._groupedBlocks.map((group) => { - return { ...group, blocks: group.blocks.filter((block) => block.label?.toLocaleLowerCase().includes(search)) }; + return { + ...group, + blocks: group.blocks.filter( + (block) => + block.label?.toLowerCase().includes(search) || + block.name?.toLowerCase().includes(search) || + block.description?.toLowerCase().includes(search), + ), + }; }); } } @@ -115,7 +174,7 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< override render() { return html` - + ${this.#renderViews()}${this.#renderMain()}
@@ -134,43 +193,40 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< } #renderClipboard() { - return html``; + return html` + + + + `; } #renderCreateEmpty() { + if (this._loading) return html`
`; return html` - ${this.data?.blocks && this.data.blocks.length > 8 - ? html` 8, + () => html` + - ` - : nothing} - ${this._filtered.map( + + `, + )} + ${repeat( + this._filtered, + (group) => group.name, (group) => html` - ${group.name && group.blocks.length !== 0 && group.name !== '' ? html`

${group.name}

` : nothing} + ${when(group.name && group.blocks.length !== 0 && group.name !== '', () => html`

${group.name}

`)}
${repeat( group.blocks, (block) => block.contentElementTypeKey, - (block) => html` - this.#chooseBlock(block.contentElementTypeKey)} - .href=${this._workspacePath && this._manager!.getContentTypeHasProperties(block.contentElementTypeKey) - ? `${this._workspacePath}create/${block.contentElementTypeKey}` - : undefined}> - - `, + (block) => this.#renderBlockTypeCard(block), )}
`, @@ -178,6 +234,32 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< `; } + #renderBlockTypeCard(block: UmbBlockTypeItemWithGroupKey) { + const href = + this._workspacePath && this._manager!.getContentTypeHasProperties(block.contentElementTypeKey) + ? `${this._workspacePath}create/${block.contentElementTypeKey}` + : undefined; + + const path = block.thumbnail ? transformServerPathToClientPath(block.thumbnail) : undefined; + const imgSrc = path ? new URL(path, this.#serverUrl)?.href : undefined; + + return html` + this.#chooseBlock(block.contentElementTypeKey)}> + ${when( + imgSrc, + (src) => html``, + () => html``, + )} + + + `; + } + #renderViews() { return html` @@ -201,14 +283,21 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement< static override styles = [ css` + #loader { + display: flex; + justify-content: center; + } + #search { width: 100%; align-items: center; margin-bottom: var(--uui-size-layout-1); + + > uui-icon { + padding-left: var(--uui-size-space-3); + } } - #search uui-icon { - padding-left: var(--uui-size-space-3); - } + .blockGroup { display: grid; gap: 1rem;