add language picker modal layout

This commit is contained in:
Mads Rasmussen
2023-02-20 13:20:35 +01:00
parent 386741e23e
commit 77916fcc16
11 changed files with 143 additions and 47 deletions

View File

@@ -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;
}
}

View File

@@ -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) };
}
/**

View File

@@ -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);

View File

@@ -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>
`;

View File

@@ -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';

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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>
`;

View File

@@ -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