From 77916fcc164e0d3b850cfa45c35c9b53c1629d40 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 20 Feb 2023 13:20:35 +0100 Subject: [PATCH] add language picker modal layout --- .../language-picker-modal-layout.element.ts | 69 +++++++++++++++++++ .../repository/language.repository.ts | 9 ++- .../languages/repository/language.store.ts | 5 ++ .../edit-language-workspace-view.element.ts | 2 +- .../src/backoffice/shared/components/index.ts | 1 + .../input-language-picker.element.ts | 32 +++++---- .../modal/layouts/modal-layout-picker-base.ts | 32 +++++---- .../picker-layout-section.element.ts | 8 +-- .../picker-layout-user-group.element.ts | 8 +-- .../picker-user/picker-layout-user.element.ts | 8 +-- .../src/core/modal/modal.service.ts | 16 ++++- 11 files changed, 143 insertions(+), 47 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts new file mode 100644 index 0000000000..7e5f9af383 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts @@ -0,0 +1,69 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { repeat } from 'lit-html/directives/repeat.js'; +import { UUIMenuItemElement, UUIMenuItemEvent } from '@umbraco-ui/uui'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { UmbModalLayoutPickerBase } from '../../../../core/modal/layouts/modal-layout-picker-base'; +import { UmbLanguageRepository } from '../repository/language.repository'; +import { LanguageModel } from '@umbraco-cms/backend-api'; + +export interface UmbLanguagePickerModalData { + multiple: boolean; + selection: string[]; +} + +@customElement('umb-language-picker-modal-layout') +export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBase { + static styles = [UUITextStyles, css``]; + + @state() + private _languages: Array = []; + + private _languageRepository = new UmbLanguageRepository(this); + + async firstUpdated() { + const { data } = await this._languageRepository.requestLanguages(); + this._languages = data?.items ?? []; + } + + #onSelection(event: UUIMenuItemEvent) { + event?.stopPropagation(); + const language = event?.target as UUIMenuItemElement; + const isoCode = language.dataset.isoCode; + if (!isoCode) return; + this.handleSelection(isoCode); + } + + render() { + return html` + + ${repeat( + this._languages, + (item) => item.isoCode, + (item) => html` + + + + ` + )} + +
+ + +
+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-language-picker-modal-layout': UmbLanguagePickerModalLayoutElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts index ea08768132..e172c4e320 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts @@ -59,17 +59,20 @@ export class UmbLanguageRepository { return { data, error, asObservable: () => this.#languageStore!.data }; } - async requestItems(isoCode: Array) { + async requestItems(isoCodes: Array) { // HACK: filter client side until we have a proper server side endpoint + // TODO: we will get a different size model here, how do we handle that in the store? const { data, error } = await this.requestLanguages(); let items = undefined; if (data) { - items = data.items = data.items.filter((x) => isoCode.includes(x.isoCode!)); + // TODO: how do we best handle this? They might have a smaller data set than the details + items = data.items = data.items.filter((x) => isoCodes.includes(x.isoCode!)); + data.items.forEach((x) => this.#languageStore?.append(x)); } - return { data: items, error }; + return { data: items, error, asObservable: () => this.#languageStore!.items(isoCodes) }; } /** diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts index eb5e1c3163..7f13331aca 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts @@ -25,6 +25,11 @@ export class UmbLanguageStore extends UmbStoreBase { remove(uniques: string[]) { this.#data.remove(uniques); } + + // TODO: how do we best handle this? They might have a smaller data set than the details + items(isoCodes: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => isoCodes.includes(item.isoCode ?? ''))); + } } export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken(UmbLanguageStore.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts index cb1427dbec..441bc067a6 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts @@ -188,7 +188,7 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement { - + `; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index 42eb476ac9..438bc8f78b 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -1,4 +1,5 @@ //TODO: we need to figure out what components should be available for extensions and load them upfront +// TODO: we need to move these files into their respective folders/silos. We then need a way for a silo to globally register a component import './backoffice-frame/backoffice-header.element'; import './backoffice-frame/backoffice-main.element'; import './backoffice-frame/backoffice-modal-container.element'; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts index 990d1e662d..8ab5c68342 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts @@ -1,12 +1,11 @@ -import { css, html, nothing } from 'lit'; +import { css, html } from 'lit'; import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { customElement, property, state } from 'lit/decorators.js'; import { ifDefined } from 'lit-html/directives/if-defined.js'; import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal'; -import { UMB_DOCUMENT_TREE_STORE_CONTEXT_TOKEN } from '../../../documents/documents/repository/document.tree.store'; import { UmbLitElement } from '@umbraco-cms/element'; -import type { FolderTreeItemModel, LanguageModel } from '@umbraco-cms/backend-api'; +import type { LanguageModel } from '@umbraco-cms/backend-api'; import type { UmbObserverController } from '@umbraco-cms/observable-api'; import { UmbLanguageRepository } from 'src/backoffice/settings/languages/repository/language.repository'; @@ -77,7 +76,7 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen private _items?: Array; private _modalService?: UmbModalService; - private _repository?: UmbLanguageRepository; + private _repository = new UmbLanguageRepository(this); private _pickedItemsObserver?: UmbObserverController; constructor() { @@ -88,18 +87,13 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen () => this.minMessage, () => !!this.min && this._selectedIsoCodes.length < this.min ); + this.addValidator( 'rangeOverflow', () => this.maxMessage, () => !!this.max && this._selectedIsoCodes.length > this.max ); - this.consumeContext('UmbLanguageRepository', (instance) => { - debugger; - this._repository = instance; - this._observePickedItems(); - }); - this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { this._modalService = instance; }); @@ -125,12 +119,13 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen multiple: this.max === 1 ? false : true, selection: [...this._selectedIsoCodes], }); + modalHandler?.onClose().then(({ selection }: any) => { this._setSelection(selection); }); } - private _removeItem(item: FolderTreeItemModel) { + private _removeItem(item: LanguageModel) { const modalHandler = this._modalService?.confirm({ color: 'danger', headline: `Remove ${item.name}?`, @@ -140,7 +135,7 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen modalHandler?.onClose().then(({ confirmed }) => { if (confirmed) { - const newSelection = this._selectedIsoCodes.filter((value) => value !== item.key); + const newSelection = this._selectedIsoCodes.filter((value) => value !== item.isoCode); this._setSelection(newSelection); } }); @@ -154,13 +149,20 @@ export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElemen render() { return html` ${this._items?.map((item) => this._renderItem(item))} - Add + Add `; } - private _renderItem(item: FolderTreeItemModel) { + private _renderItem(item: LanguageModel) { return html` - + this._removeItem(item)} label="Remove ${item.name}">Remove diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts index a8ad224698..51f0b8cc03 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts @@ -1,4 +1,4 @@ -import { state } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import { UmbModalLayoutElement } from '..'; export interface UmbPickerData { @@ -6,45 +6,47 @@ export interface UmbPickerData { selection: Array; } +// TODO: we should consider moving this into a class/context instead of an element. +// So we don't have to extend an element to get basic picker/selection logic export class UmbModalLayoutPickerBase extends UmbModalLayoutElement> { - @state() - private _selection: Array = []; + @property() + selection: Array = []; connectedCallback(): void { super.connectedCallback(); - this._selection = this.data?.selection || []; + this.selection = this.data?.selection || []; } - protected _submit() { - this.modalHandler?.close({ selection: this._selection }); + submit() { + this.modalHandler?.close({ selection: this.selection }); } - protected _close() { + close() { this.modalHandler?.close(); } protected _handleKeydown(e: KeyboardEvent, key: selectType) { if (e.key === 'Enter') { - this._handleItemClick(key); + this.handleSelection(key); } } /* TODO: Write test for this select/deselect method. */ - protected _handleItemClick(key: selectType) { + handleSelection(key: selectType) { if (this.data?.multiple) { - if (this._isSelected(key)) { - this._selection = this._selection.filter((selectedKey) => selectedKey !== key); + if (this.isSelected(key)) { + this.selection = this.selection.filter((selectedKey) => selectedKey !== key); } else { - this._selection.push(key); + this.selection.push(key); } } else { - this._selection = [key]; + this.selection = [key]; } this.requestUpdate('_selection'); } - protected _isSelected(key: selectType): boolean { - return this._selection.includes(key); + isSelected(key: selectType): boolean { + return this.selection.includes(key); } } diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts index 21b0608262..361e18acc1 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts @@ -73,9 +73,9 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase { ${this._sections.map( (item) => html`
this._handleItemClick(item.alias)} + @click=${() => this.handleSelection(item.alias)} @keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.alias)} - class=${this._isSelected(item.alias) ? 'item selected' : 'item'}> + class=${this.isSelected(item.alias) ? 'item selected' : 'item'}> ${item.meta.label}
` @@ -83,8 +83,8 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase {
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts index d05474cf87..4e32140f45 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts @@ -82,9 +82,9 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase { ${this._userGroups.map( (item) => html`
this._handleItemClick(item.key)} + @click=${() => this.handleSelection(item.key)} @keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.key)} - class=${this._isSelected(item.key) ? 'item selected' : 'item'}> + class=${this.isSelected(item.key) ? 'item selected' : 'item'}> ${item.name}
@@ -93,8 +93,8 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase {
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts index 452d26ca4f..ca74c4aefb 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts @@ -86,9 +86,9 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase { ${this._users.map( (item) => html`
this._handleItemClick(item.key)} + @click=${() => this.handleSelection(item.key)} @keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.key)} - class=${this._isSelected(item.key) ? 'item selected' : 'item'}> + class=${this.isSelected(item.key) ? 'item selected' : 'item'}> ${item.name}
@@ -97,8 +97,8 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase {
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index 0ed747171e..85ec632abd 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -5,6 +5,7 @@ import './layouts/media-picker/modal-layout-media-picker.element'; import './layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element'; import './layouts/modal-layout-current-user.element'; import './layouts/icon-picker/modal-layout-icon-picker.element'; +import '../../backoffice/settings/languages/language-picker/language-picker-modal-layout.element'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; import { BehaviorSubject } from 'rxjs'; @@ -16,6 +17,7 @@ import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-edit import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layout-media-picker.element'; import { UmbModalHandler } from './modal-handler'; import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbLanguagePickerModalData } from '../../backoffice/settings/languages/language-picker/language-picker-modal-layout.element'; export type UmbModalType = 'dialog' | 'sidebar'; @@ -25,7 +27,9 @@ export interface UmbModalOptions { data?: UmbModalData; } -// TODO: Should this be called UmbModalContext ? as we don't have 'services' as a term. +// TODO: rename to UmbModalContext +// TODO: we should find a way to easily open a modal without adding custom methods to this context. It would result in a better separation of concerns. +// TODO: move all layouts into their correct "silo" folders. User picker should live with users etc. export class UmbModalService { // TODO: Investigate if we can get rid of HTML elements in our store, so we can use one of our states. #modals = new BehaviorSubject(>[]); @@ -106,6 +110,16 @@ export class UmbModalService { return this.open('umb-modal-layout-change-password', { data, type: 'dialog' }); } + /** + * Opens a language picker sidebar modal + * @public + * @return {*} {UmbModalHandler} + * @memberof UmbModalService + */ + public languagePicker(data: UmbLanguagePickerModalData): UmbModalHandler { + return this.open('umb-language-picker-modal-layout', { data, type: 'sidebar' }); + } + /** * Opens a modal or sidebar modal * @public