add language picker modal layout
This commit is contained in:
@@ -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<LanguageModel> = [];
|
||||
|
||||
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`<umb-body-layout headline="Select languages">
|
||||
<uui-box>
|
||||
${repeat(
|
||||
this._languages,
|
||||
(item) => item.isoCode,
|
||||
(item) => html`
|
||||
<uui-menu-item
|
||||
label=${item.name ?? ''}
|
||||
selectable="true"
|
||||
@selected=${this.#onSelection}
|
||||
@unselected=${this.#onSelection}
|
||||
?selected=${this.isSelected(item.isoCode!)}
|
||||
data-iso-code="${ifDefined(item.isoCode)}">
|
||||
<uui-icon slot="icon" name="umb:globe"></uui-icon>
|
||||
</uui-menu-item>
|
||||
`
|
||||
)}
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label="Close" @click=${this.close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this.submit}></uui-button>
|
||||
</div>
|
||||
</umb-body-layout> `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'umb-language-picker-modal-layout': UmbLanguagePickerModalLayoutElement;
|
||||
}
|
||||
}
|
||||
@@ -59,17 +59,20 @@ export class UmbLanguageRepository {
|
||||
return { data, error, asObservable: () => this.#languageStore!.data };
|
||||
}
|
||||
|
||||
async requestItems(isoCode: Array<string>) {
|
||||
async requestItems(isoCodes: Array<string>) {
|
||||
// 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) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<string>) {
|
||||
return this.#data.getObservablePart((items) => items.filter((item) => isoCodes.includes(item.isoCode ?? '')));
|
||||
}
|
||||
}
|
||||
|
||||
export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken<UmbLanguageStore>(UmbLanguageStore.name);
|
||||
|
||||
@@ -188,7 +188,7 @@ export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement {
|
||||
<umb-workspace-property-layout
|
||||
label="Fallback language"
|
||||
description="To allow multi-lingual content to fall back to another language if not present in the requested language, select it here.">
|
||||
<umb-input-language-picker slot="editor"></umb-input-language-picker>
|
||||
<umb-input-language-picker slot="editor" max="1"></umb-input-language-picker>
|
||||
</umb-workspace-property-layout>
|
||||
</uui-box>
|
||||
`;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<LanguageModel>;
|
||||
|
||||
private _modalService?: UmbModalService;
|
||||
private _repository?: UmbLanguageRepository;
|
||||
private _repository = new UmbLanguageRepository(this);
|
||||
private _pickedItemsObserver?: UmbObserverController<LanguageModel>;
|
||||
|
||||
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))}
|
||||
<uui-button id="add-button" look="placeholder" @click=${this._openPicker} label="open">Add</uui-button>
|
||||
<uui-button
|
||||
id="add-button"
|
||||
look="placeholder"
|
||||
@click=${this._openPicker}
|
||||
label="open"
|
||||
?disabled="${this._selectedIsoCodes.length === this.max}"
|
||||
>Add</uui-button
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderItem(item: FolderTreeItemModel) {
|
||||
private _renderItem(item: LanguageModel) {
|
||||
return html`
|
||||
<!-- TODO: add ref language -->
|
||||
<!-- TODO: add language ref element -->
|
||||
<uui-ref-node name=${ifDefined(item.name === null ? undefined : item.name)} detail=${ifDefined(item.key)}>
|
||||
<uui-action-bar slot="actions">
|
||||
<uui-button @click=${() => this._removeItem(item)} label="Remove ${item.name}">Remove</uui-button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { UmbModalLayoutElement } from '..';
|
||||
|
||||
export interface UmbPickerData<selectType = string> {
|
||||
@@ -6,45 +6,47 @@ export interface UmbPickerData<selectType = string> {
|
||||
selection: Array<selectType>;
|
||||
}
|
||||
|
||||
// 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<selectType = string> extends UmbModalLayoutElement<UmbPickerData<selectType>> {
|
||||
@state()
|
||||
private _selection: Array<selectType> = [];
|
||||
@property()
|
||||
selection: Array<selectType> = [];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase {
|
||||
${this._sections.map(
|
||||
(item) => html`
|
||||
<div
|
||||
@click=${() => 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'}>
|
||||
<span>${item.meta.label}</span>
|
||||
</div>
|
||||
`
|
||||
@@ -83,8 +83,8 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase {
|
||||
</div>
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label="Close" @click=${this._close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
|
||||
<uui-button label="Close" @click=${this.close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this.submit}></uui-button>
|
||||
</div>
|
||||
</umb-workspace-layout>
|
||||
`;
|
||||
|
||||
@@ -82,9 +82,9 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase {
|
||||
${this._userGroups.map(
|
||||
(item) => html`
|
||||
<div
|
||||
@click=${() => 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'}>
|
||||
<uui-icon .name=${item.icon}></uui-icon>
|
||||
<span>${item.name}</span>
|
||||
</div>
|
||||
@@ -93,8 +93,8 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase {
|
||||
</div>
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label="Close" @click=${this._close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
|
||||
<uui-button label="Close" @click=${this.close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this.submit}></uui-button>
|
||||
</div>
|
||||
</umb-workspace-layout>
|
||||
`;
|
||||
|
||||
@@ -86,9 +86,9 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase {
|
||||
${this._users.map(
|
||||
(item) => html`
|
||||
<div
|
||||
@click=${() => 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'}>
|
||||
<uui-avatar .name=${item.name}></uui-avatar>
|
||||
<span>${item.name}</span>
|
||||
</div>
|
||||
@@ -97,8 +97,8 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase {
|
||||
</div>
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label="Close" @click=${this._close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this._submit}></uui-button>
|
||||
<uui-button label="Close" @click=${this.close}></uui-button>
|
||||
<uui-button label="Submit" look="primary" color="positive" @click=${this.submit}></uui-button>
|
||||
</div>
|
||||
</umb-workspace-layout>
|
||||
`;
|
||||
|
||||
@@ -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<UmbModalData> {
|
||||
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(<Array<UmbModalHandler>>[]);
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user