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 2f45cdca03..72fc1695ab 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 @@ -702,6 +702,10 @@ export const data: Array = [ alias: 'blockGroups', value: [{ key: 'demo-block-group-id', name: 'Demo Blocks' }], }, + { + alias: 'layoutStylesheet', + value: '/wwwroot/css/umbraco-blockgridlayout.css' + }, { alias: 'blocks', value: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts index 15ae596750..134e48ac35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts @@ -1,7 +1,7 @@ import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../types.js'; import type { UmbBlockGridWorkspaceData } from '../index.js'; import { UmbArrayState, appendToFrozenArray, pushAtToUniqueArray } from '@umbraco-cms/backoffice/observable-api'; -import { removeInitialSlashFromPath, transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils'; +import { removeLastSlashFromPath, transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; @@ -29,7 +29,7 @@ export class UmbBlockGridManagerContext< if (layoutStylesheet) { // Cause we await initAppUrl in setting the _editorConfiguration, we can trust the appUrl begin here. - return this.#appUrl! + removeInitialSlashFromPath(transformServerPathToClientPath(layoutStylesheet)); + return removeLastSlashFromPath(this.#appUrl!) + transformServerPathToClientPath(layoutStylesheet); } return undefined; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts index 6be3216d4b..9edc38b487 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/block-type-card/block-type-card.element.ts @@ -6,13 +6,13 @@ import { html, customElement, property, state, ifDefined } from '@umbraco-cms/ba import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; -import { removeInitialSlashFromPath, transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils'; +import { removeLastSlashFromPath, transformServerPathToClientPath } from '@umbraco-cms/backoffice/utils'; @customElement('umb-block-type-card') export class UmbBlockTypeCardElement extends UmbLitElement { // #init: Promise; - #appUrl?: string; + #appUrl: string = ''; #itemManager = new UmbRepositoryItemsManager( this, @@ -28,7 +28,7 @@ export class UmbBlockTypeCardElement extends UmbLitElement { value = transformServerPathToClientPath(value); if (value) { this.#init.then(() => { - this._iconFile = this.#appUrl + removeInitialSlashFromPath(value); + this._iconFile = removeLastSlashFromPath(this.#appUrl) + value; }); } else { this._iconFile = undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts index dccd64dad7..4bb332af04 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/types.ts @@ -1,4 +1,5 @@ import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/extension-registry'; +export type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/extension-registry'; export interface UmbBlockTypeGroup { name?: string; 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 c551ab0506..483767c50a 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 @@ -12,6 +12,7 @@ export * from './path/path-decode.function.js'; export * from './path/path-encode.function.js'; export * from './path/path-folder-name.function.js'; export * from './path/remove-initial-slash-from-path.function.js'; +export * from './path/remove-last-slash-from-path.function.js'; export * from './path/stored-path.function.js'; export * from './path/transform-server-path-to-client-path.function.js'; export * from './path/umbraco-path.function.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-initial-slash-from-path.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-initial-slash-from-path.function.ts index 64ab8b51c8..f2b26ac1c6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-initial-slash-from-path.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-initial-slash-from-path.function.ts @@ -1,5 +1,5 @@ /** - * + * Removes the initial slash from a path, if the first character is a slash. * @param path */ export function removeInitialSlashFromPath(path: string) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-last-slash-from-path.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-last-slash-from-path.function.ts new file mode 100644 index 0000000000..5973746c29 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/path/remove-last-slash-from-path.function.ts @@ -0,0 +1,7 @@ +/** + * Remove the last slash from a path, if the last character is a slash. + * @param path + */ +export function removeLastSlashFromPath(path: string) { + return path.endsWith('/') ? path.slice(undefined, -1) : path; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts index e7d0046b8e..e0c3ec8ed1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts @@ -1,9 +1,13 @@ import { UmbMemberCollectionRepository } from '../../collection/index.js'; +import { UmbMemberSearchProvider } from '../../search/member.search-provider.js'; import type { UmbMemberDetailModel } from '../../types.js'; +import type { UmbMemberItemModel } from '../../repository/index.js'; import type { UmbMemberPickerModalValue, UmbMemberPickerModalData } from './member-picker-modal.token.js'; -import { html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { debounce, UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-member-picker-modal') export class UmbMemberPickerModalElement extends UmbModalBaseElement< @@ -13,8 +17,18 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< @state() private _members: Array = []; + @state() + private _searchQuery: string = ''; + + @state() + private _searchResult: Array = []; + + @state() + private _searching = false; + #collectionRepository = new UmbMemberCollectionRepository(this); #selectionManager = new UmbSelectionManager(this); + #searchProvider = new UmbMemberSearchProvider(this); override connectedCallback(): void { super.connectedCallback(); @@ -23,6 +37,18 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< this.#selectionManager.setSelection(this.value?.selection ?? []); } + constructor() { + super(); + this.observe( + this.#selectionManager.selection, + (selection) => { + this.updateValue({ selection }); + this.requestUpdate(); + }, + 'umbSelectionObserver', + ); + } + override async firstUpdated() { const { data } = await this.#collectionRepository.requestCollection({}); this._members = data?.items ?? []; @@ -36,43 +62,143 @@ export class UmbMemberPickerModalElement extends UmbModalBaseElement< } } - #submit() { - this.value = { selection: this.#selectionManager.getSelection() }; - this.modalContext?.submit(); + #onSearchInput(event: UUIInputEvent) { + const value = event.target.value as string; + this._searchQuery = value; + + if (!this._searchQuery) { + this._searchResult = []; + this._searching = false; + return; + } + + this._searching = true; + this.#debouncedSearch(); } - #close() { - this.modalContext?.reject(); + #debouncedSearch = debounce(this.#search, 300); + + async #search() { + if (!this._searchQuery) return; + const { data } = await this.#searchProvider.search({ query: this._searchQuery }); + this._searchResult = data?.items ?? []; + this._searching = false; + } + + #onSearchClear() { + this._searchQuery = ''; + this._searchResult = []; } override render() { - return html` - - ${repeat( - this.#filteredMembers, - (item) => item.unique, - (item) => html` - this.#selectionManager.select(item.unique)} - @deselected=${() => this.#selectionManager.deselect(item.unique)} - ?selected=${this.#selectionManager.isSelected(item.unique)}> - - + return html` + + ${this.#renderSearch()} ${this.#renderItems()} +
+ this.modalContext?.reject()}> + this.modalContext?.submit()}> +
+
+ `; + } + + #renderItems() { + if (this._searchQuery) return nothing; + return html` + ${repeat( + this.#filteredMembers, + (item) => item.unique, + (item) => this.#renderMemberItem(item), + )} + `; + } + + #renderSearch() { + return html` + +
+ ${this._searching + ? html`` + : html``} +
+ ${when( + this._searchQuery, + () => html` +
+ + + +
`, )} -
-
- - -
-
`; + +
+ ${this.#renderSearchResult()} + `; } + + #renderSearchResult() { + if (this._searchQuery && this._searching === false && this._searchResult.length === 0) { + return this.#renderEmptySearchResult(); + } + + return html` + ${repeat( + this._searchResult, + (item) => item.unique, + (item) => this.#renderMemberItem(item), + )} + `; + } + + #renderEmptySearchResult() { + return html`No result for "${this._searchQuery}".`; + } + + #renderMemberItem(item: UmbMemberItemModel | UmbMemberDetailModel) { + return html` + this.#selectionManager.select(item.unique)} + @deselected=${() => this.#selectionManager.deselect(item.unique)} + ?selected=${this.#selectionManager.isSelected(item.unique)}> + + + `; + } + + static override styles = [ + UmbTextStyles, + css` + #search-input { + width: 100%; + } + + #search-divider { + width: 100%; + height: 1px; + background-color: var(--uui-color-divider); + margin-top: var(--uui-size-space-5); + margin-bottom: var(--uui-size-space-3); + } + + #search-indicator { + margin-left: 7px; + margin-top: 4px; + } + `, + ]; } export default UmbMemberPickerModalElement;